using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NTwain.Triplets;
using NTwain.Data;
using NTwain.Values;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Windows.Interop;
using System.Diagnostics;
using System.Security.Permissions;
using System.IO;
using System.ComponentModel;
using System.Threading;

namespace NTwain
{
    /// <summary>
    /// Provides a session for working with TWAIN api in an application.
    /// </summary>
    public class TwainSession : ITwainStateInternal, IMessageFilter, INotifyPropertyChanged
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="TwainSession" /> class.
        /// </summary>
        /// <param name="appId">The app id.</param>
        /// <exception cref="System.ArgumentNullException"></exception>
        public TwainSession(TWIdentity appId)
        {
            if (appId == null) { throw new ArgumentNullException("appId"); }
            _appId = appId;
            State = 1;
            EnforceState = true;
        }

        #region properties

        object _callbackObj;
        SynchronizationContext _syncer;
        TWIdentity _appId;


        /// <summary>
        /// Gets the app id used for the session.
        /// </summary>
        /// <value>The app id.</value>
        TWIdentity ITwainStateInternal.GetAppId() { return _appId; }

        /// <summary>
        /// Gets the source id used for the session.
        /// </summary>
        /// <value>The source id.</value>
        public TWIdentity SourceId { get; private set; }

        /// <summary>
        /// Gets the current state number as defined by the TWAIN spec.
        /// </summary>
        /// <value>The state.</value>
        public int State { get; private set; }

        /// <summary>
        /// Gets or sets a value indicating whether callback is used parts of source communication
        /// if supported. May be required if things don't work. This does not take effect if
        /// the source is already open.
        /// </summary>
        /// <value>
        ///   <c>true</c> to disable callback; otherwise, <c>false</c>.
        /// </value>
        public bool DisableCallback { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether calls to triplets will verify the current twain session state.
        /// </summary>
        /// <value>
        ///   <c>true</c> if state value is enforced; otherwise, <c>false</c>.
        /// </value>
        public bool EnforceState { get; set; }

        DGAudio _dgAudio;
        /// <summary>
        /// Gets the triplet operations defined for audio data group.
        /// </summary>
        /// <value>The DG audio.</value>
        public DGAudio DGAudio
        {
            get
            {
                if (_dgAudio == null) { _dgAudio = new DGAudio(this); }
                return _dgAudio;
            }
        }

        DGControl _dgControl;
        /// <summary>
        /// Gets the triplet operations defined for control data group.
        /// </summary>
        /// <value>The DG control.</value>
        public DGControl DGControl
        {
            get
            {
                if (_dgControl == null) { _dgControl = new DGControl(this); }
                return _dgControl;
            }
        }

        DGImage _dgImage;
        /// <summary>
        /// Gets the triplet operations defined for image data group.
        /// </summary>
        /// <value>The DG image.</value>
        public DGImage DGImage
        {
            get
            {
                if (_dgImage == null) { _dgImage = new DGImage(this); }
                return _dgImage;
            }
        }

        private IList<CapabilityId> _supportedCaps;
        /// <summary>
        /// Gets the supported caps for the current source.
        /// </summary>
        /// <value>
        /// The supported caps.
        /// </value>
        public IList<CapabilityId> SupportedCaps
        {
            get
            {
                if (_supportedCaps == null && State > 3)
                {
                    _supportedCaps = this.GetCapabilities();
                }
                return _supportedCaps ?? new CapabilityId[0];
            }
            private set
            {
                _supportedCaps = value;
                RaisePropertyChanged("SupportedCaps");
            }
        }


        #endregion

        #region state transition calls

        void ITwainStateInternal.ChangeState(int newState, bool notifyChange)
        {
            Debug.WriteLine("TWAIN State = " + newState);
            State = newState;
            if (notifyChange) { RaisePropertyChanged("State"); }
        }
        ICommitable ITwainStateInternal.GetPendingStateChanger(int newState)
        {
            return new TentativeStateCommitable(this, newState);
        }
        void ITwainStateInternal.ChangeSourceId(TWIdentity sourceId)
        {
            SourceId = sourceId;
            RaisePropertyChanged("SourceId");
        }


        HandleRef _parentHandle;

        /// <summary>
        /// Opens the data source manager.
        /// </summary>
        /// <param name="handle">The handle. On Windows = points to the window handle (hWnd) that will act as the Source’s
        /// "parent". On Macintosh = should be a NULL value.</param>
        /// <returns></returns>
        public ReturnCode OpenManager(HandleRef handle)
        {
            Debug.WriteLine(string.Format("Thread {0}: OpenManager.", Thread.CurrentThread.ManagedThreadId));

            _parentHandle = handle;
            var rc = DGControl.Parent.OpenDsm(handle.Handle);
            if (rc == ReturnCode.Success)
            {
                // if twain2 then get mem management stuff
                if ((_appId.DataFunctionalities & DataFunctionalities.Dsm2) == DataFunctionalities.Dsm2)
                {
                    TWEntryPoint entry;
                    rc = DGControl.EntryPoint.Get(out entry);
                    if (rc == ReturnCode.Success)
                    {
                        MemoryManager.Instance.UpdateEntryPoint(entry);
                        Debug.WriteLine("Using TWAIN2 memory functions.");
                    }
                    else
                    {
                        CloseManager();
                    }
                }
            }
            return rc;
        }

        /// <summary>
        /// Closes the data source manager.
        /// </summary>
        /// <returns></returns>
        public ReturnCode CloseManager()
        {
            Debug.WriteLine(string.Format("Thread {0}: CloseManager.", Thread.CurrentThread.ManagedThreadId));

            var rc = DGControl.Parent.CloseDsm(_parentHandle.Handle);
            if (rc == ReturnCode.Success)
            {
                _parentHandle = default(HandleRef);
                MemoryManager.Instance.UpdateEntryPoint(null);
            }
            return rc;
        }

        /// <summary>
        /// Loads the specified source into main memory and causes its initialization.
        /// </summary>
        /// <param name="sourceProductName">Name of the source.</param>
        /// <returns></returns>
        public ReturnCode OpenSource(string sourceProductName)
        {
            var source = new TWIdentity();
            source.ProductName = sourceProductName;
            return OpenSource(source);
        }

        /// <summary>
        /// Loads the specified Source into main memory and causes its initialization.
        /// </summary>
        /// <param name="sourceId">The source id.</param>
        /// <returns></returns>
        public ReturnCode OpenSource(TWIdentity sourceId)
        {
            if (sourceId == null) { throw new ArgumentNullException("sourceId"); }

            Debug.WriteLine(string.Format("Thread {0}: OpenSource.", Thread.CurrentThread.ManagedThreadId));

            var rc = DGControl.Identity.OpenDS(sourceId);
            if (rc == ReturnCode.Success)
            {
                SupportedCaps = this.GetCapabilities();

                // TODO: does it work?
                _syncer = SynchronizationContext.Current ?? new SynchronizationContext();

                if (!DisableCallback)
                {
                    // app v2.2 or higher uses callback2
                    if (_appId.ProtocolMajor >= 2 && _appId.ProtocolMinor >= 2)
                    {
                        var cb = new TWCallback2(CallbackHandler);
                        var rc2 = DGControl.Callback2.RegisterCallback(cb);

                        if (rc2 == ReturnCode.Success)
                        {
                            Debug.WriteLine("Registered callback2.");
                            _callbackObj = cb;
                        }
                    }
                    else
                    {
                        var cb = new TWCallback(CallbackHandler);

                        var rc2 = DGControl.Callback.RegisterCallback(cb);

                        if (rc2 == ReturnCode.Success)
                        {
                            Debug.WriteLine("Registered callback.");
                            _callbackObj = cb;
                        }
                    }
                }
            }
            return rc;
        }

        ReturnCode CallbackHandler(TWIdentity origin, TWIdentity dest,
            DataGroups dg, DataArgumentType dat, Values.Message msg, IntPtr data)
        {
            if (origin != null && SourceId != null && origin.Id == SourceId.Id)
            {
                Debug.WriteLine(string.Format("Thread {0}: GOT TWAIN callback for msg {1}.", Thread.CurrentThread.ManagedThreadId, msg));
                // spec says should handle this on the thread that enabled the DS, 
                // but it's already the same and doesn't work (failure + seqError) w/o jumping to another thread and back.
                // My guess is the DS needs to see the Success first before letting transfer happen
                // so this is an artificial delay to make it happen.
                // TODO: find a better method.
                ThreadPool.QueueUserWorkItem(o =>
                {
                    _syncer.Send(blah =>
                    {
                        HandleSourceMsg(origin, dest, dg, dat, msg, data);
                    }, null);
                }, null);
                return ReturnCode.Success;
            }
            return ReturnCode.Failure;
        }


        /// <summary>
        /// When an application is finished with a Source, it must formally close the session between them
        /// using this operation. This is necessary in case the Source only supports connection with a single
        /// application (many desktop scanners will behave this way). A Source such as this cannot be
        /// accessed by other applications until its current session is terminated
        /// </summary>
        /// <returns></returns>
        public ReturnCode CloseSource()
        {
            Debug.WriteLine(string.Format("Thread {0}: CloseSource.", Thread.CurrentThread.ManagedThreadId));

            var rc = DGControl.Identity.CloseDS();
            if (rc == ReturnCode.Success)
            {
                _callbackObj = null;
                SupportedCaps = null;
            }
            return rc;
        }

        TWUserInterface _twui;
        /// <summary>
        /// Enables the source for data acquisition.
        /// </summary>
        /// <param name="mode">The mode.</param>
        /// <param name="modal">if set to <c>true</c> any driver UI will display as modal.</param>
        /// <param name="windowHandle">The window handle if modal.</param>
        /// <returns></returns>
        public ReturnCode EnableSource(SourceEnableMode mode, bool modal, HandleRef windowHandle)
        {
            Debug.WriteLine(string.Format("Thread {0}: EnableSource.", Thread.CurrentThread.ManagedThreadId));

            _twui = new TWUserInterface();
            _twui.ShowUI = mode == SourceEnableMode.ShowUI;
            _twui.ModalUI = modal;
            _twui.hParent = windowHandle.Handle;

            if (mode == SourceEnableMode.ShowUIOnly)
            {
                return DGControl.UserInterface.EnableDSUIOnly(_twui);
            }
            else
            {
                return DGControl.UserInterface.EnableDS(_twui);
            }
        }

        /// <summary>
        /// Disables the source to end data acquisition.
        /// </summary>
        /// <returns></returns>
        ReturnCode DisableSource()
        {
            Debug.WriteLine(string.Format("Thread {0}: DisableSource.", Thread.CurrentThread.ManagedThreadId));

            var rc = DGControl.UserInterface.DisableDS(_twui);
            if (rc == ReturnCode.Success)
            {
                var hand = SourceDisabled;
                if (hand != null)
                {
                    try
                    {
                        hand(this, EventArgs.Empty);
                    }
                    catch { }
                }
            }
            return rc;
        }

        #endregion

        #region consumer to handle

        /// <summary>
        /// Occurs when source has been disabled (back to state 4).
        /// </summary>
        public event EventHandler SourceDisabled;
        /// <summary>
        /// Occurs when a data transfer is ready.
        /// </summary>
        public event EventHandler<TransferReadyEventArgs> TransferReady;
        /// <summary>
        /// Occurs when the source has generated an event.
        /// </summary>
        public event EventHandler<DeviceEventArgs> DeviceEvent;
        /// <summary>
        /// Occurs when data has been transferred.
        /// </summary>
        public event EventHandler<DataTransferredEventArgs> DataTransferred;


        private void DoTransferRoutine()
        {
            TWPendingXfers pending = new TWPendingXfers();
            var rc = ReturnCode.Success;

            do
            {
                IList<FileFormat> formats = Enumerable.Empty<FileFormat>().ToList();
                IList<Compression> compressions = Enumerable.Empty<Compression>().ToList();
                bool canDoFileXfer = this.CapGetImageXferMech().Contains(XferMech.File);
                var curFormat = this.GetCurrentCap<FileFormat>(CapabilityId.ICapImageFileFormat);
                var curComp = this.GetCurrentCap<Compression>(CapabilityId.ICapCompression);
                TWImageInfo imgInfo;
                bool skip = false;
                if (DGImage.ImageInfo.Get(out imgInfo) != ReturnCode.Success)
                {
                    // bad!
                    skip = true;
                }

                try
                {
                    formats = this.CapGetImageFileFormat();
                }
                catch { }
                try
                {
                    compressions = this.CapGetCompression();
                }
                catch { }

                // ask consumer for cancel in case of non-ui multi-page transfers
                TransferReadyEventArgs args = new TransferReadyEventArgs(pending, formats, curFormat, compressions,
                    curComp, canDoFileXfer, imgInfo);
                args.CancelCurrent = skip;

                var hand = TransferReady;
                if (hand != null)
                {
                    try
                    {
                        hand(this, args);
                    }
                    catch { }
                }

                if (!args.CancelAll && !args.CancelCurrent)
                {
                    Values.XferMech mech = this.GetCurrentCap<XferMech>(CapabilityId.ICapXferMech);

                    if (args.CanDoFileXfer && !string.IsNullOrEmpty(args.OutputFile))
                    {
                        var setXferRC = DGControl.SetupFileXfer.Set(new TWSetupFileXfer
                        {
                            FileName = args.OutputFile,
                            Format = args.ImageFormat
                        });
                        if (setXferRC == ReturnCode.Success)
                        {
                            mech = XferMech.File;
                        }
                    }

                    // I don't know how this is supposed to work so it probably doesn't
                    //this.CapSetImageFormat(args.ImageFormat);
                    //this.CapSetImageCompression(args.ImageCompression);

                    #region do xfer

                    // TODO: expose all swallowed exceptions somehow later

                    IntPtr dataPtr = IntPtr.Zero;
                    IntPtr lockedPtr = IntPtr.Zero;
                    string file = null;
                    try
                    {
                        ReturnCode xrc = ReturnCode.Cancel;
                        switch (mech)
                        {
                            case Values.XferMech.Native:
                                xrc = DGImage.ImageNativeXfer.Get(ref dataPtr);
                                break;
                            case Values.XferMech.File:
                                xrc = DGImage.ImageFileXfer.Get();
                                if (File.Exists(args.OutputFile))
                                {
                                    file = args.OutputFile;
                                }
                                break;
                            case Values.XferMech.MemFile:
                                // not supported yet
                                //TWImageMemXfer memxfer = new TWImageMemXfer();
                                //xrc = DGImage.ImageMemXfer.Get(memxfer);
                                break;
                        }
                        if (xrc == ReturnCode.XferDone)
                        {
                            State = 7;
                            try
                            {
                                var dtHand = DataTransferred;
                                if (dtHand != null)
                                {
                                    if (dataPtr != IntPtr.Zero)
                                    {
                                        lockedPtr = MemoryManager.Instance.Lock(dataPtr);
                                    }
                                    dtHand(this, new DataTransferredEventArgs(lockedPtr, file));
                                }
                            }
                            catch { }
                        }
                        //}
                        //else if (group == DataGroups.Audio)
                        //{
                        //	var xrc = DGAudio.AudioNativeXfer.Get(ref dataPtr);
                        //	if (xrc == ReturnCode.XferDone)
                        //	{
                        //		State = 7;
                        //		try
                        //		{
                        //			var dtHand = DataTransferred;
                        //			if (dtHand != null)
                        //			{
                        //				lockedPtr = MemoryManager.Instance.MemLock(dataPtr);
                        //				dtHand(this, new DataTransferredEventArgs(lockedPtr));
                        //			}
                        //		}
                        //		catch { }
                        //	}
                        //}
                    }
                    finally
                    {
                        State = 6;
                        // data here is allocated by source so needs to use shared mem calls
                        if (lockedPtr != IntPtr.Zero)
                        {
                            MemoryManager.Instance.Unlock(lockedPtr);
                            lockedPtr = IntPtr.Zero;
                        }
                        if (dataPtr != IntPtr.Zero)
                        {
                            MemoryManager.Instance.Free(dataPtr);
                            dataPtr = IntPtr.Zero;
                        }
                    }
                    #endregion
                }

                if (args.CancelAll)
                {
                    rc = DGControl.PendingXfers.Reset(pending);
                    if (rc == ReturnCode.Success)
                    {
                        // if audio exit here
                        //if (group == DataGroups.Audio)
                        //{
                        //	//???
                        //	return;
                        //}

                    }
                }
                else
                {
                    rc = DGControl.PendingXfers.EndXfer(pending);
                }
            } while (rc == ReturnCode.Success && pending.Count != 0);

            State = 5;
            DisableSource();

        }

        #endregion

        #region messaging use

        ReturnCode HandleSourceMsg(TWIdentity origin, TWIdentity destination,
            DataGroups dg, DataArgumentType dat, NTwain.Values.Message msg, IntPtr data)
        {
            Debug.WriteLine(string.Format("Thread {0}: HandleSourceMsg at state {1} with DG={2} DAT={3} MSG={4}.", Thread.CurrentThread.ManagedThreadId, State, dg, dat, msg));

            ReturnCode rc = ReturnCode.Success;

            switch (msg)
            {
                case Values.Message.XferReady:
                    if (State < 6)
                        State = 6;
                    // this is the meat of all twain stuff
                    DoTransferRoutine();
                    break;
                case Values.Message.DeviceEvent:
                    TWDeviceEvent de;
                    rc = DGControl.DeviceEvent.Get(out de);
                    if (rc == ReturnCode.Success)
                    {
                        var hand = this.DeviceEvent;
                        if (hand != null)
                        {
                            try
                            {
                                hand(this, new DeviceEventArgs(de));
                            }
                            catch { }
                        }
                    }
                    break;
                case Values.Message.CloseDSReq:
                case Values.Message.CloseDSOK:
                    // even though it says closeDS it's really disable.
                    // dsok is sent if source is enabled with uionly

                    // some sources send this at other states so do a step down
                    if (State > 5)
                    {
                        ForceStepDown(4);
                    }
                    else if (State == 5)
                    {
                        // needs this state check since some source sends this more than once
                        DisableSource();
                    }
                    break;
            }

            return rc;
        }

        /// <summary>
        /// Forces the stepping down of an opened source ignoring return values.
        /// Used when session state and source state become out of sync.
        /// </summary>
        /// <param name="targetState">State of the target.</param>
        public void ForceStepDown(int targetState)
        {
            Debug.WriteLine(string.Format("Thread {0}: ForceStepDown.", Thread.CurrentThread.ManagedThreadId));

            bool origFlag = EnforceState;
            EnforceState = false;

            // From the twain spec
            // Stepping Back Down the States
            // DG_CONTROL / DAT_PENDINGXFERS / MSG_ENDXFER → state 7 to 6
            // DG_CONTROL / DAT_PENDINGXFERS / MSG_RESET → state 6 to 5
            // DG_CONTROL / DAT_USERINTERFACE / MSG_DISABLEDS → state 5 to 4
            // DG_CONTROL / DAT_IDENTITY / MSG_CLOSEDS → state 4 to 3
            // Ignore the status returns from the calls prior to the one yielding the desired state. For instance, if a
            // call during scanning returns TWCC_SEQERROR and the desire is to return to state 5, then use the
            // following commands.
            // DG_CONTROL / DAT_PENDINGXFERS / MSG_ENDXFER → state 7 to 6
            // DG_CONTROL / DAT_PENDINGXFERS / MSG_RESET → state 6 to 5
            // Being sure to confirm that DG_CONTROL / DAT_PENDINGXFERS / MSG_RESET returned
            // success, the return status from DG_CONTROL / DAT_PENDINGXFERS / MSG_ENDXFER may
            // be ignored.

            if (targetState < 7)
            {
                DGControl.PendingXfers.EndXfer(new TWPendingXfers());
            }
            if (targetState < 6)
            {
                DGControl.PendingXfers.Reset(new TWPendingXfers());
            }
            if (targetState < 5)
            {
                DisableSource();
            }
            if (targetState < 4)
            {
                CloseSource();
            }
            if (targetState < 3)
            {
                CloseManager();
            }
            EnforceState = origFlag;
        }

        /// <summary>
        /// Handles the message from a message loop.
        /// </summary>
        /// <param name="msgPtr">Pointer to message structure.</param>
        /// <returns>True if handled by TWAIN.</returns>
        bool HandleLoopMsgEvent(ref IntPtr msgPtr)
        {
            TWEvent evt = new TWEvent();
            evt.pEvent = msgPtr;
            var rc = DGControl.Event.ProcessEvent(evt);
            HandleSourceMsg(null, null, DataGroups.Control, DataArgumentType.Null, evt.TWMessage, IntPtr.Zero);
            return rc == ReturnCode.DSEvent;
        }

        /// <summary>
        /// Message loop processor for winform. 
        /// Use this by adding the <see cref="TwainSession"/> as an <see cref="IMessageFilter "/>.
        /// </summary>
        /// <param name="m">The message to be dispatched. You cannot modify this message.</param>
        /// <returns>
        /// true to filter the message and stop it from being dispatched; false to allow the message to continue to the next filter or control.
        /// </returns>
        //[EnvironmentPermissionAttribute(SecurityAction.LinkDemand)]
        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        bool IMessageFilter.PreFilterMessage(ref System.Windows.Forms.Message m)
        {
            if (State > 3)
            {
                MSG winmsg = default(MSG);
                winmsg.hwnd = m.HWnd;
                winmsg.lParam = m.LParam;
                winmsg.message = m.Msg;
                winmsg.wParam = m.WParam;

                IntPtr msgPtr = IntPtr.Zero;
                try
                {
                    // no need to lock for marshal alloc
                    //msgPtr = MemoryManager.Instance.Allocate((uint)Marshal.SizeOf(winmsg));
                    msgPtr = Marshal.AllocHGlobal(Marshal.SizeOf(winmsg));
                    Marshal.StructureToPtr(winmsg, msgPtr, false);
                    return HandleLoopMsgEvent(ref msgPtr);
                }
                finally
                {
                    if (msgPtr != IntPtr.Zero)
                        Marshal.FreeHGlobal(msgPtr);
                        //MemoryManager.Instance.Free(msgPtr);
                }
            }
            return false;
        }

        /// <summary>
        /// Message loop processor for wpf.
        /// Use this as the target of <see cref="HwndSourceHook"/> delegate.
        /// </summary>
        /// <param name="hwnd">The window handle.</param>
        /// <param name="msg">The message ID.</param>
        /// <param name="wParam">The message's wParam value.</param>
        /// <param name="lParam">The message's lParam value.</param>
        /// <param name="handled">A value that indicates whether the message was handled. Set the value to true if the message was handled; otherwise, false.</param>
        /// <returns></returns>
        [EnvironmentPermissionAttribute(SecurityAction.LinkDemand)]
        public IntPtr PreFilterMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            // always pass message since it works whether there's a callback or not?
            if (State > 3)// && _callbackObj == null)
            {
                MSG winmsg = default(MSG);
                winmsg.hwnd = hwnd;
                winmsg.lParam = lParam;
                winmsg.message = msg;
                winmsg.wParam = wParam;

                IntPtr msgPtr = IntPtr.Zero;
                try
                {
                    // no need to lock for marshal alloc
                    //msgPtr = MemoryManager.Instance.Allocate((uint)Marshal.SizeOf(winmsg));
                    msgPtr = Marshal.AllocHGlobal(Marshal.SizeOf(winmsg));
                    Marshal.StructureToPtr(winmsg, msgPtr, false);
                    handled = HandleLoopMsgEvent(ref msgPtr);
                }
                finally
                {
                    if (msgPtr != IntPtr.Zero)
                        Marshal.FreeHGlobal(msgPtr);
                    //MemoryManager.Instance.Free(msgPtr);
                }
            }
            return IntPtr.Zero;
        }
        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        void RaisePropertyChanged(string property)
        {
            var hand = PropertyChanged;
            if (hand != null) { hand(this, new PropertyChangedEventArgs(property)); }
        }

        #endregion

    }
}