using NTwain.Data;
using NTwain.Triplets;
using NTwain.Values;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace NTwain
{
    /// <summary>
    /// Base class for interfacing with TWAIN.
    /// </summary>
    public class TwainSessionBase : ITwainStateInternal, ITwainOperation
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="TwainSessionOld" /> class.
        /// </summary>
        /// <param name="appId">The app id.</param>
        /// <exception cref="System.ArgumentNullException"></exception>
        public TwainSessionBase(TWIdentity appId)
        {
            if (appId == null) { throw new ArgumentNullException("appId"); }
            _appId = appId;
            State = 1;
            EnforceState = true;
        }

        TWIdentity _appId;
        HandleRef _appHandle;
        SynchronizationContext _syncer;
        object _callbackObj; // kept around so it doesn't get gc'ed
        TWUserInterface _twui;


        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");
            }
        }


        #region ITwainStateInternal Members

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

        /// <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; }

        void ITwainStateInternal.ChangeState(int newState, bool notifyChange)
        {
            _state = newState;
            if (notifyChange)
            {
                RaisePropertyChanged("State");
                OnStateChanged();
            }
        }

        ICommitable ITwainStateInternal.GetPendingStateChanger(int newState)
        {
            return new TentativeStateCommitable(this, newState);
        }

        void ITwainStateInternal.ChangeSourceId(TWIdentity sourceId)
        {
            SourceId = sourceId;
            RaisePropertyChanged("SourceId");
            OnSourceChanged();
        }

        #endregion

        #region ITwainState Members

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

        int _state;
        /// <summary>
        /// Gets the current state number as defined by the TWAIN spec.
        /// </summary>
        /// <value>
        /// The state.
        /// </value>
        public int State
        {
            get { return _state; }
            protected set
            {
                if (value > 0 && value < 8)
                {
                    _state = value;
                    RaisePropertyChanged("State");
                    OnStateChanged();
                }
            }
        }

        #endregion

        #region ITwainOperation Members

        DGAudio _dgAudio;
        /// <summary>
        /// Gets the triplet operations defined for audio data group.
        /// </summary>
        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>
        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>
        public DGImage DGImage
        {
            get
            {
                if (_dgImage == null) { _dgImage = new DGImage(this); }
                return _dgImage;
            }
        }

        #endregion

        #region INotifyPropertyChanged Members

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

        /// <summary>
        /// Raises the <see cref="PropertyChanged"/> event.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        protected void RaisePropertyChanged(string propertyName)
        {
            var hand = PropertyChanged;
            if (hand != null) { hand(this, new PropertyChangedEventArgs(propertyName)); }
        }

        #endregion

        #region privileged calls that causes state change in TWAIN

        /// <summary>
        /// Opens the data source manager. This must be the first method used
        /// before using other TWAIN functions. Calls to this must be followed by <see cref="CloseManager"/> when done.
        /// </summary>
        /// <param name="appHandle">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 appHandle)
        {
            Debug.WriteLine(string.Format("Thread {0}: OpenManager.", Thread.CurrentThread.ManagedThreadId));

            var rc = DGControl.Parent.OpenDsm(appHandle.Handle);
            if (rc == ReturnCode.Success)
            {
                _appHandle = appHandle;
                // if twain2 then get memory management functions
                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(_appHandle.Handle);
            if (rc == ReturnCode.Success)
            {
                _appHandle = default(HandleRef);
            }
            return rc;
        }

        /// <summary>
        /// Loads the specified source into main memory and causes its initialization.
        /// Calls to this must be followed by 
        /// <see cref="CloseSource" /> when done.
        /// </summary>
        /// <param name="sourceProductName">Name of the source.</param>
        /// <returns></returns>
        /// <exception cref="ArgumentException">Source name is required.;sourceProductName</exception>
        public ReturnCode OpenSource(string sourceProductName)
        {
            if (string.IsNullOrEmpty(sourceProductName)) { throw new ArgumentException("Source name is required.", "sourceProductName"); }

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

            var source = new TWIdentity();
            source.ProductName = sourceProductName;

            var rc = DGControl.Identity.OpenDS(source);
            if (rc == ReturnCode.Success)
            {
            }
            return rc;
        }

        /// <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;
            }
            return rc;
        }

        /// <summary>
        /// Enables the source.
        /// </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>
        /// <param name="context">The
        /// Windows only. 
        /// <see cref="SynchronizationContext" /> that is required for certain operations.
        /// It is recommended you call this method in an UI thread and pass in
        /// <see cref="SynchronizationContext.Current" />
        /// if you do not have a custom one setup.</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">context</exception>
        public ReturnCode EnableSource(SourceEnableMode mode, bool modal, HandleRef windowHandle, SynchronizationContext context)
        {
            if (Environment.OSVersion.Platform == PlatformID.Win32NT)
            {
                if (context == null) { throw new ArgumentNullException("context"); }
            }

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

            _syncer = context;

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

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

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

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

            _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>
        protected ReturnCode DisableSource()
        {
            Debug.WriteLine(string.Format("Thread {0}: DisableSource.", Thread.CurrentThread.ManagedThreadId));

            var rc = DGControl.UserInterface.DisableDS(_twui);
            if (rc == ReturnCode.Success)
            {
                OnSourceDisabled();
            }
            return rc;
        }

        /// <summary>
        /// Forces the stepping down of an opened source when things gets out of control.
        /// 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;
        }

        #endregion

        #region custom events and overridables

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

        /// <summary>
        /// Called when <see cref="State"/> changed.
        /// </summary>
        protected virtual void OnStateChanged()
        {
            var hand = StateChanged;
            if (hand != null)
            {
                try
                {
                    hand(this, EventArgs.Empty);
                }
                catch { }
            }
        }

        /// <summary>
        /// Called when <see cref="SourceId"/> changed.
        /// </summary>
        protected virtual void OnSourceChanged()
        {
            var hand = SourceChanged;
            if (hand != null)
            {
                try
                {
                    hand(this, EventArgs.Empty);
                }
                catch { }
            }
        }

        /// <summary>
        /// Called when source has been disabled (back to state 4).
        /// </summary>
        protected virtual void OnSourceDisabled()
        {
            var hand = SourceDisabled;
            if (hand != null)
            {
                try
                {
                    hand(this, EventArgs.Empty);
                }
                catch { }
            }
        }

        /// <summary>
        /// Raises the <see cref="E:DeviceEvent" /> event.
        /// </summary>
        /// <param name="e">The <see cref="DeviceEventArgs"/> instance containing the event data.</param>
        protected virtual void OnDeviceEvent(DeviceEventArgs e)
        {
            var hand = DeviceEvent;
            if (hand != null)
            {
                try
                {
                    hand(this, e);
                }
                catch { }
            }
        }

        /// <summary>
        /// Raises the <see cref="E:TransferReady" /> event.
        /// </summary>
        /// <param name="e">The <see cref="TransferReadyEventArgs"/> instance containing the event data.</param>
        protected virtual void OnTransferReady(TransferReadyEventArgs e)
        {
            var hand = TransferReady;
            if (hand != null)
            {
                try
                {
                    hand(this, e);
                }
                catch { }
            }
        }

        /// <summary>
        /// Raises the <see cref="E:DataTransferred" /> event.
        /// </summary>
        /// <param name="e">The <see cref="DataTransferredEventArgs"/> instance containing the event data.</param>
        protected virtual void OnDataTransferred(DataTransferredEventArgs e)
        {
            var hand = DataTransferred;
            if (hand != null)
            {
                try
                {
                    hand(this, e);
                }
                catch { }
            }
        }

        #endregion

        #region real TWAIN logic during xfer

        /// <summary>
        /// Handles the message from a typical WndProc message loop and check if it's from the TWAIN source.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <returns>True if handled by TWAIN.</returns>
        protected bool HandleWndProcMessage(ref MESSAGE message)
        {
            var handled = false;
            if (State >= 4) // technically we should only handle on state >= 5 but there might be missed msgs if we wait until state changes after enabling ds
            {
                // transform it into a pointer for twain
                IntPtr msgPtr = IntPtr.Zero;
                try
                {
                    // no need to do another lock call when using marshal alloc
                    msgPtr = Marshal.AllocHGlobal(Marshal.SizeOf(message));
                    Marshal.StructureToPtr(message, msgPtr, false);

                    TWEvent evt = new TWEvent();
                    evt.pEvent = msgPtr;
                    handled = DGControl.Event.ProcessEvent(evt) == ReturnCode.DSEvent;
                    
                    HandleSourceMsg(evt.TWMessage);
                }
                finally
                {
                    if (msgPtr != IntPtr.Zero) { Marshal.FreeHGlobal(msgPtr); }
                }
            }
            return handled;
        }

        ReturnCode HandleCallback(TWIdentity origin, TWIdentity destination, DataGroups dg, DataArgumentType dat, Message msg, IntPtr data)
        {
            if (origin != null && SourceId != null && origin.Id == SourceId.Id)
            {
                Debug.WriteLine(string.Format("Thread {0}: CallbackHandler at state {1} with DG={2} DAT={3} MSG={4}.", Thread.CurrentThread.ManagedThreadId, State, dg, dat, msg));
                // spec says we must handle this on the thread that enabled the DS, 
                // but it's usually already the same thread and doesn't work (failure + seqError) w/o jumping to another thread and back.
                // My guess is the DS needs to see the Success return code first before letting transfer happen
                // so this is an hack to make it happen.

                // TODO: find a better method.
                ThreadPool.QueueUserWorkItem(o =>
                {
                    var ctx = o as SynchronizationContext;
                    if (ctx != null)
                    {
                        _syncer.Send(blah =>
                        {
                            HandleSourceMsg(msg);
                        }, null);
                    }
                    else
                    {
                        // no context? better hope for the best!
                        HandleSourceMsg(msg);
                    }
                }, _syncer);
                return ReturnCode.Success;
            }
            return ReturnCode.Failure;
        }

        // method that handles msg from the source, whether it's from wndproc or callbacks
        void HandleSourceMsg(Message msg)
        {
            if (msg != Message.Null)
            {
                Debug.WriteLine(string.Format("Thread {0}: HandleSourceMsg at state {1} with MSG={2}.", Thread.CurrentThread.ManagedThreadId, State, msg));
            }

            switch (msg)
            {
                case Message.XferReady:
                    if (State < 6)
                    {
                        State = 6;
                    }
                    DoTransferRoutine();
                    break;
                case Message.DeviceEvent:
                    TWDeviceEvent de;
                    var rc = DGControl.DeviceEvent.Get(out de);
                    if (rc == ReturnCode.Success)
                    {
                        OnDeviceEvent(new DeviceEventArgs(de));
                    }
                    break;
                case Message.CloseDSReq:
                case 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;
            }
        }

        /// <summary>
        /// Does the TWAIN transfer routine at state 6. 
        /// </summary>
        /// <exception cref="NotImplementedException"></exception>
        protected virtual void DoTransferRoutine()
        {
            throw new NotImplementedException();
        }

        #endregion



        /// <summary>
        /// The MSG structure in Windows for TWAIN use.
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        protected struct MESSAGE
        {
            public IntPtr hwnd;
            public uint message;
            public IntPtr wParam;
            public IntPtr lParam;
            public uint time;
            public int x;
            public int y;
        }
    }
}