using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Windows;

using Caliburn.Micro;

using FDOT.GIS.Client.Commands;
using FDOT.GIS.Client.Controls;
using FDOT.GIS.Client.Core;
using FDOT.GIS.Client.Domain;
using FDOT.GIS.Client.Domain.Gis;
using FDOT.GIS.Client.Domain.Persistence;
using FDOT.GIS.DataContracts.Enums;
using FDOT.GIS.Framework.Examples.Views;


namespace FDOT.GIS.Framework.Examples.ViewModels
{
    [Export]
    [PartCreationPolicy(CreationPolicy.Shared)]
    [DisplayName("Query By Address")]
    public class QueryByAddressViewModel : ComponentBase
    {
        public QueryByAddressViewModel() { }


        #region public properties for view binding
        /// <summary>
        /// The following are all binding properties for QueryByAddressView.  Where appropriate, they
        /// call NotifyOfPropertyChange to alert the view of value changes.
        /// </summary>

        #region feature type display/selection
        public bool LimitQueriesToActiveLayers 
        {
            get { return QueryByAddressSettingsViewModel.Instance.LimitQueriesToActiveLayers; } 
        }

        public IEnumerable<FeatureType> FeatureTypes
        {
            get
            { return LimitQueriesToActiveLayers ? MapHandler.ActiveFeatureTypes : MapHandler.AllFeatureTypes; }
        }

        public FeatureType SelectedFeatureType 
        {
            get { return QueryParameters.FeatureType; }
            set { QueryParameters.FeatureType = value; }
        }
        #endregion


        #region search address
        private AddressCommandParameters _address = new AddressCommandParameters { State = "FL" };
        public AddressCommandParameters Address
        {
            get { return _address; }
            set
            {
                _address = value;
                NotifyOfPropertyChange(() => Address);
            }
        }


        public bool IsValidSearchAddress
        {
            get { return !string.IsNullOrEmpty(Address.StreetAddress); }
        }
        #endregion


        #region buffer
        public double BufferRadius { get; set; }

        private ObservableCollection<string> _bufferUnits = null;
        public ObservableCollection<string> BufferUnits
        {
            get
            {
                if (_bufferUnits == null)
                { _bufferUnits = new ObservableCollection<string> { "Feet", "Meters", "Miles", "Kilometers" }; }
                return _bufferUnits;
            }
        }

        private string _bufferUnit = null;
        public string BufferUnit
        {
            get { return _bufferUnit; }
            set
            {
                _bufferUnit = value;
                NotifyOfPropertyChange(() => BufferUnit);
            }
        }
        #endregion

        
        // This controls the visibility of the busy indicator, which displays while the query is running.
        private bool _isBusy;
        public bool IsBusy
        {
            get { return _isBusy; }
            set
            {
                if (value == _isBusy) return;
                _isBusy = value;
                NotifyOfPropertyChange(() => IsBusy);
            }
        }
        #endregion


        #region event handlers

        /// <summary>
        /// Binding the units dropdown occurs after the default value is set (initialization with custom settings), so this
        /// event is fired after the dropdown is loaded to apply the default unit.
        /// </summary>
        private string DefaultBufferUnit = "Feet";
        public void SetDefaultUnit()
        { BufferUnit = string.IsNullOrEmpty(BufferUnit) ? DefaultBufferUnit : BufferUnit; }


        public void ValidateAddressSearchCriteria()
        { NotifyOfPropertyChange(() => IsValidSearchAddress); }


        /// <summary>
        /// This event is triggered when the DropDownTreeView items have been loaded.
        /// Here, it's being used to move recently queried feature types to their own 
        /// group named "Recently Queried", then move that group to the top of the list.
        /// If recent queries aren't going to be saved (see GetResultsWorkflow() below)
        /// then this handler can be omitted.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void ItemsLoaded(object sender, EventArgs e)
        {
            var ddTree = sender as DropdownTreeView;
            if (ddTree == null) return;
            if (StateManager.QueriedFeatureTypes != null && StateManager.QueriedFeatureTypes.Count > 0)
            {
                ddTree.AssignItemsToGroup(StateManager.QueriedFeatureTypes, "Recently Queried");
                ddTree.MoveGroupToTop("Recently Queried");
            }
            else
            { ddTree.DefaultGroupName = "All Layers"; }
        }
        #endregion


        #region query execution

        /// <summary>
        /// QueryParameters are the required argument for ExecuteQuery, so this will be modified during the workflow to
        /// produce the final query that will be executed.
        /// </summary>
        private QueryParameters _queryParameters = null;
        private QueryParameters QueryParameters
        {
            get
            {
                if (_queryParameters == null)
                {
                    _queryParameters = new QueryParameters();
                    _queryParameters.GeometryCriteria = new GeometryCriteria { SpatialRelationship = QuerySpatialRelationships.Intersects };
                }
                return _queryParameters;
            }
        }

        /// <summary>
        /// 1.  Call GetAddressLocationsCommand to get points based on the address provided.
        /// </summary>
        public void ExecuteQuery()
        {
            IsBusy = true;                                          // show the busy indicator

            var command = new GetAddressLocationsCommand();         // initialize a new instance of GetAddressLocationsCommand
            command.Initialize(null);
            command.ExecutionComplete += OnAddressLocated;          // add a completed event handler

            try 
            { command.Execute(_address); }                          // execute the command
            catch (Exception ex) 
            { HandleError(ex); }
        }

        /// <summary>
        /// 2a.  Convert the points returned into a buffered area to use for our query.
        /// 
        /// Note: This is called when the GetAddressLocationsCommand executed in ExecuteQuery() has completed.  
        /// We're using this as the trigger to start the next command in the sequence.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnAddressLocated(object sender, CommandExecutionCompleteEventArgs e)
        {
            var results = e.Result as AddressLocatorResult;
            var addressLocation = results.Features.FirstOrDefault();            // get the first feature returned, which is the closest match
            if (addressLocation != null)
            {
                // Use that location to set the spatial criterion 
                QueryParameters.GeometryCriteria.Geometry = addressLocation.Geometry;

                // Apply a buffer to the spatial query criterion.
                // Note that this will call BufferComplete when it's done, so we can start the next command in the chain.
                Coroutine.BeginExecute(ApplyBufferWorkflow(QueryParameters.GeometryCriteria.Geometry).GetEnumerator(), null, BufferComplete);
            }
            else
            { 
                LayoutManager.Instance.ShowMessage("No results found for query", new TimeSpan(0, 0, 0, 8));
                IsBusy = false;
            }
        }

        /// <summary>
        /// 2b.  Apply a buffer to the spatial query criterion.
        /// </summary>
        /// <param name="geometry"></param>
        /// <returns></returns>
        public IEnumerable<IResult> ApplyBufferWorkflow(DataContracts.Geometry geometry)
        {
            bool doApplyBuffer = BufferRadius > 0 && !string.IsNullOrEmpty(BufferUnit);
            if (!doApplyBuffer) yield break;

            // There are at least a couple ways to buffer geometry, this being one of them.  The BufferGeometryCommand is another option.
            // Both ultimately use the same service and will produce the same result.  Buffer extends ResultBase so it can be used
            // in a couroutine as has been done here with ApplyBufferWorkflow.
            var buffer = new FDOT.GIS.Client.Domain.Buffer.Buffer(BufferRadius, BufferUnit, geometry);
            yield return buffer;

            // Set the query spatial criterion to the buffered geometry
            QueryParameters.GeometryCriteria.Geometry = buffer.Result;
        }

        /// <summary>
        /// 3a.  The buffer has been applied so, assuming there are no errors, execute the query.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BufferComplete(object sender, ResultCompletionEventArgs e)
        {
            if (e.Error != null) HandleError(e.Error);
            Coroutine.BeginExecute(GetResultsWorkflow().GetEnumerator(), new ActionExecutionContext(), QueryComplete);
        }

        /// <summary>
        /// 3b.  Execute the query and build the results view.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<IResult> GetResultsWorkflow()
        {
            var queryWorkFlow = new ExecuteQuery(QueryParameters);          // execute the query via the ExecuteQuery command
            yield return queryWorkFlow;

            // Saves the query if StateManager.DoPersistRecentItems == true.
            // That setting is managed through the User Preferences Dialog and can also be set programmatically.
            StateManager.SaveRecentQuery(QueryParameters);                  

            var features = queryWorkFlow.Result.Features;                   // if there are any features returned
            if (features.Any())
            {
                string label = QueryParameters.FeatureType.Attributes.Single(a => a.IsIdentifier).Name;     // get an identifier to use for map tip label
                var args = new ResultDisplayArgs(queryWorkFlow.Result)                                      // and build ResultDisplayArgs for the result view
                {
                    MapTipColumn = label,
                    MapTipLabel = label,
                    Title = QueryParameters.FeatureType.Name
                };
                // send the features and display args to ResultDisplayManager to construct the results view
                ResultDisplayManager.Instance.DisplayResults(args);                                         
            }
            else
            { LayoutManager.Instance.ShowMessage("No results found for query", new TimeSpan(0, 0, 0, 8)); }

            IsBusy = false;     // clear the busy indicator
            yield break;
        }

        /// <summary>
        /// 4.  The results have been displayed, so zoom to the envelope queried.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void QueryComplete(object sender, ResultCompletionEventArgs e)
        {
            // note that the buffer for the zoom parameters is to provide some space around the envelope
            // so the graphics are hidden behind dialogs.
            var zoom = new ZoomToGeometryCommand();
            zoom.Execute(new ZoomToGeometryCommandParameters
                                {
                                    Geometry = QueryParameters.GeometryCriteria.Geometry,
                                    Buffer = 2.0                                            
                                });
        }

        #endregion


        /// <summary>
        /// The following section contains the overrides for FDOT.GIS.Client.Domain.ComponentBase. 
        /// </summary>
        #region IComponentBase implementation
        /// <summary>
        /// IconResourceUri defines the image that will be used for the dialog icon and the icon on the component tool bar.
        /// The path refers to the Silverlight resource path and can be broken down as follows:
        /// 
        /// Example "/FDOT.GIS.Client;component/Assets/Images/query.png":
        ///     /FDOT.GIS.Client            -- the assembly containing the resource
        ///     component/                  -- the root of that assembly
        ///     Assets/Images/query.png     -- the path to the resource from the root of the assembly
        /// </summary>
        protected override string IconResourceUri
        {
            get { return "/FDOT.GIS.Client;component/Assets/Images/query.png"; }
        }


        /// <summary>
        /// Called by the Initialize method on the component when the component hasn't yet been initialized.
        /// </summary>
        /// <param name="customSettings">Custom settings are passed through from the component configuration 
        /// in the virtual application configuration.  They can be used here to provide any special initialization 
        /// parameters to the component.</param>
        protected override void OnInitialize(IDictionary<string, string> customSettings)
        {
            // for this example, customSettings are being used to pre-populate some of the search parameters
            if (customSettings.ContainsKey("DefaultAddress")) Address.StreetAddress = customSettings["DefaultAddress"];
            if (customSettings.ContainsKey("DefaultCity")) Address.City = customSettings["DefaultCity"];
            if (customSettings.ContainsKey("DefaultBufferUnit")) DefaultBufferUnit = customSettings["DefaultBufferUnit"];

            double bufferRadius = 0;
            if (customSettings.ContainsKey("DefaultBufferRadius")) double.TryParse(customSettings["DefaultBufferRadius"], out bufferRadius);
            BufferRadius = bufferRadius;
        }


        /// <summary>
        /// Called when the component is activated on the tool bar.  Here we initialize the view in a GenericDialog that handles
        /// the title and window options (e.g. minimize, close).
        /// </summary>
        protected override void OnExecute()
        {
            // A logger is provided via ComponentBase
            Log.Info("Entered {0}.OnExecute().", this.GetType().ToString());

            var view = new QueryByAddressView();
            View.SetModel(view, this);
            LayoutManager.Instance.ShowPanel(view,
                                          new DialogServiceArgs
                                          {
                                              Title = ComponentConfiguration.Name,
                                              HelpId = "tools;" + GetType().FullName,
                                              Icon = Icon,
                                              ShowSettingsOnBack = true,
                                              SettingsViewModel = QueryByAddressSettingsViewModel.Instance
                                          });

        }


        /// <summary>
        /// This is called if there's an error during component initialization.  Other errors in the component
        /// must still be handled.
        /// </summary>
        /// <param name="errorEventArgs"></param>
        protected override void OnError(ErrorEventArgs errorEventArgs)
        {
            HandleError(errorEventArgs.Exception);
            errorEventArgs.Handled = true;
        }


        /// <summary>
        /// Common code for component error handling.
        /// </summary>
        /// <param name="ex"></param>
        private void HandleError(Exception ex)
        {
            Log.Error(ex);
            MessageBox.Show(ex.ToString());
            IsBusy = false;
        }

        #endregion
    }
}
