// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

// 
// Description:
//      JournalNavigationScope is the entity that owns a Journal and handles or dispatches journaling-
//      related operations in a tree of navigators sharing the same journal. 
//      NavigationWindow aggregates a JournalNavigationScope instance, as it always needs to have a 
//      journal. So does Frame but only when its JournalOwnership=OwnsJournal.
//      JournalNavigationScope is not a standalone class. It delegate certain operations to its 
//      "navigator host" via the IJournalNavigationScopeHost interface or directly to the host's 
//      NavigationService.
//

using System;
using System.Collections;
using System.Security;
using System.Diagnostics;

using System.Windows;
using System.Windows.Navigation;
using MS.Internal.KnownBoxes;


namespace MS.Internal.AppModel
{
    internal class JournalNavigationScope : DependencyObject, INavigator
    {
        internal JournalNavigationScope(IJournalNavigationScopeHost host)
        {
            _host = host;
            _rootNavSvc = host.NavigationService;
        }

        #region DependencyProperties
        // These properties are declared here, but actual values are set on NavigationWindow and Frame.
        // See OnBackForwardStateChange().

        // CanGoBack & CanGoForward DPs

        private static readonly DependencyPropertyKey CanGoBackPropertyKey =
            DependencyProperty.RegisterReadOnly(
                    "CanGoBack", typeof(bool), typeof(JournalNavigationScope),
                    new FrameworkPropertyMetadata(BooleanBoxes.FalseBox));

        internal static readonly DependencyProperty CanGoBackProperty =
            CanGoBackPropertyKey.DependencyProperty;

        private static readonly DependencyPropertyKey CanGoForwardPropertyKey =
            DependencyProperty.RegisterReadOnly(
                    "CanGoForward", typeof(bool), typeof(JournalNavigationScope),
                    new FrameworkPropertyMetadata(BooleanBoxes.FalseBox));

        internal static readonly DependencyProperty CanGoForwardProperty =
            CanGoForwardPropertyKey.DependencyProperty;

        // BackStack & ForwardStack DPs

        private static readonly DependencyPropertyKey BackStackPropertyKey =
                DependencyProperty.RegisterReadOnly(
                        "BackStack", typeof(IEnumerable), typeof(JournalNavigationScope),
                        new FrameworkPropertyMetadata((IEnumerable)null));

        internal static readonly DependencyProperty BackStackProperty =
                BackStackPropertyKey.DependencyProperty;

        private static readonly DependencyPropertyKey ForwardStackPropertyKey =
                DependencyProperty.RegisterReadOnly(
                        "ForwardStack", typeof(IEnumerable), typeof(JournalNavigationScope),
                        new FrameworkPropertyMetadata((IEnumerable)null));

        internal static readonly DependencyProperty ForwardStackProperty =
                ForwardStackPropertyKey.DependencyProperty;

        #endregion DependencyProperties

        #region INavigatorBase Members

        public Uri Source
        {
            get { return _host.Source; }
            set { _host.Source = value; }
        }
        public Uri CurrentSource
        {
            get { return _host.CurrentSource; }
        }
        public object Content
        {
            get { return _host.Content; }
            set { _host.Content = value; }
        }

        public bool Navigate(Uri source)
        {
            return _host.Navigate(source);
        }
        public bool Navigate(Uri source, object extraData)
        {
            return _host.Navigate(source, extraData);
        }
        public bool Navigate(object content)
        {
            return _host.Navigate(content);
        }
        public bool Navigate(object content, object extraData)
        {
            return _host.Navigate(content, extraData);
        }

        public void StopLoading()
        {
            _host.StopLoading();
        }

        public void Refresh()
        {
            _host.Refresh();
        }

        public event NavigatingCancelEventHandler Navigating
        {
            add { _host.Navigating += value; }
            remove { _host.Navigating -= value; }
        }
        public event NavigationProgressEventHandler NavigationProgress
        {
            add { _host.NavigationProgress += value; }
            remove { _host.NavigationProgress -= value; }
        }
        public event NavigationFailedEventHandler NavigationFailed
        {
            add { _host.NavigationFailed += value; }
            remove { _host.NavigationFailed -= value; }
        }
        public event NavigatedEventHandler Navigated
        {
            add { _host.Navigated += value; }
            remove { _host.Navigated -= value; }
        }
        public event LoadCompletedEventHandler LoadCompleted
        {
            add { _host.LoadCompleted += value; }
            remove { _host.LoadCompleted -= value; }
        }
        public event NavigationStoppedEventHandler NavigationStopped
        {
            add { _host.NavigationStopped += value; }
            remove { _host.NavigationStopped -= value; }
        }
        public event FragmentNavigationEventHandler FragmentNavigation
        {
            add { _host.FragmentNavigation += value; }
            remove { _host.FragmentNavigation -= value; }
        }

        #endregion INavigatorBase

        #region INavigator Members
        //
        // Because the INavigator methods of NavigationWindow and Frame simply forward here,
        // _host.VerifyContextAndObjectState() should be called at every entry point.

        public bool CanGoForward
        {
            get
            {
                _host.VerifyContextAndObjectState();
                return _journal != null && !InAppShutdown && _journal.CanGoForward;
            }
        }
        public bool CanGoBack
        {
            get
            {
                _host.VerifyContextAndObjectState();
                return _journal != null && !InAppShutdown && _journal.CanGoBack;
            }
        }

        public void GoForward()
        {
            // CanGoForward checks the calling thread and InAppShutdown as well
            if (CanGoForward == false)
                throw new InvalidOperationException(SR.NoForwardEntry);

            if (!_host.GoForwardOverride())
            {
                JournalEntry je = Journal.BeginForwardNavigation();
                // If je is null it indicates that we are going going "forward" to the currently
                // displayed page which has no journal entry. The comment in BeginForwardNavigation
                // explains how this can happen.
                if (je == null)
                {
                    _rootNavSvc.StopLoading();
                    return;
                }
                NavigateToEntry(je);
                // NavigateToEntry() should call AbortJournalNavigation() if navigation is canceled.
            }
        }

        public void GoBack()
        {
            // CanGoBack checks the calling thread and InAppShutdown as well
            if (CanGoBack == false)
                throw new InvalidOperationException(SR.NoBackEntry);

            if (!_host.GoBackOverride())
            {
                JournalEntry je = Journal.BeginBackNavigation();
                if (je == null) // See comment in GoForwardInternal().
                {
                    _rootNavSvc.StopLoading();
                    return;
                }
                NavigateToEntry(je);
                // NavigateToEntry() should call Journal.AbortJournalNavigation() if navigation is canceled.
            }
        }

        public void AddBackEntry(CustomContentState state)
        {
            _host.VerifyContextAndObjectState();
            _rootNavSvc.AddBackEntry(state);
        }

        public JournalEntry RemoveBackEntry()
        {
            _host.VerifyContextAndObjectState();
            return _journal == null ? null : _journal.RemoveBackEntry();
        }

        public System.Collections.IEnumerable BackStack
        {
            get
            {
                _host.VerifyContextAndObjectState();
                return Journal.BackStack;
            }
        }

        public System.Collections.IEnumerable ForwardStack
        {
            get
            {
                _host.VerifyContextAndObjectState();
                return Journal.ForwardStack;
            }
        }

        JournalNavigationScope INavigator.GetJournal(bool create)
        {
            return this;
        }

        #endregion INavigator

        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------
        #region Internal Methods

        /// <summary>
        /// The Journal instance needs to be lazily-created to allow restoring a deserialized journal
        /// from the TravelLog or from a child Frame's journaled state (Frame.FramePersistState).
        /// Call this method to make the back stack and forward stack objects available and to allow
        /// anyone to register journal change event handlers.
        /// </summary>
        internal void EnsureJournal()
        {
            // The getter lazily creates Journal and notifies the host.
            Journal journal = Journal;
            Debug.Assert(journal != null);
        }

        internal bool CanInvokeJournalEntry(int entryId)
        {
            // _journal could be null for .deploy apps since the main (second) app is created after a delay
            // (the first app is created to show the progress UI and the main app is created when bits are ready)
            if (_journal == null)
                return false;

            int realIndex = _journal.FindIndexForEntryWithId(entryId);
            if (realIndex == -1)
                return false;

            JournalEntry entry = _journal[realIndex];
            // Journal.IsNavigable() will apply the filter, which is normally IsEntryNavigable().
            return _journal.IsNavigable(entry);
        }

        internal bool NavigateToEntry(int index)
        {
            JournalEntry entry = Journal[index];
            return NavigateToEntry(entry);
        }

        internal bool NavigateToEntry(JournalEntry entry)
        {
            if (entry == null)
            {
                Debug.Fail("Tried to navigate to a null JournalEntry.");
                return false;
            }
            if (!Journal.IsNavigable(entry))
            {
                Debug.Fail("Tried to navigate to a non-navigable journal entry.");
                return false;
            }

            NavigationService navigationService = _rootNavSvc.FindTarget(entry.NavigationServiceId);
            Debug.Assert(navigationService != null, "NavigationService cannot be null for journal navigations");

            NavigationMode mode = Journal.GetNavigationMode(entry);
            bool navigated = false;
            try
            {
                navigated = entry.Navigate(navigationService.INavigatorHost, mode);
            }
            finally
            {
                if (!navigated)
                {
                    AbortJournalNavigation();
                }
            }
            return navigated;
        }

        internal void AbortJournalNavigation()
        {
            if (_journal != null)
            {
                _journal.AbortJournalNavigation();
            }
        }

        internal INavigatorBase FindTarget(string name)
        {
            return _rootNavSvc.FindTarget(name);
        }

        internal static void ClearDPValues(DependencyObject navigator)
        {
            navigator.SetValue(CanGoBackPropertyKey, BooleanBoxes.FalseBox);
            navigator.SetValue(CanGoForwardPropertyKey, BooleanBoxes.FalseBox);

            navigator.SetValue(BackStackPropertyKey, null);
            navigator.SetValue(ForwardStackPropertyKey, null);
        }

#if DEBUG
        /// <summary>
        /// DO NOT USE - Public debug method to test the journal state for PageFunctions
        /// </summary>
        internal string PrintJournal()
        {
            Journal journal = Journal;
            String s = "";
            for (int i = 0; i < journal.TotalCount; i++)
            {
                if (journal[i].IsNavigable())
                {
                    s += "o"  ;
                }
                else
                {
                    switch (journal[i].EntryType)
                    {
                        case JournalEntryType.Navigable:
                            s += "o";
                            break;
                        case JournalEntryType.UiLess:
                            s += "u";
                            break;
                        default:
                            Invariant.Assert(false, $"Invalid JournalEntryType: {journal[i].EntryType}");
                            break;
                    }
                }
            }
            return s;
        }
#endif
        #endregion

        //------------------------------------------------------
        //
        //  Internal Properties
        //
        //------------------------------------------------------
        #region Internal Properties

        /// <summary>
        /// The Journal instance needs to be lazily-created to allow restoring a deserialized journal
        /// from the TravelLog or from a child Frame's journaled state (Frame.FramePersistState).
        /// The getter triggers the Journal creation and notifes the host.
        /// <seealso cref="EnsureJournal"/>
        /// </summary>
#if DEBUG
        // to prevent creating the Journal instance prematurely while debugging
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
#endif
        internal Journal Journal
        {
            get
            {
                if (_journal == null)
                {
                    Journal = new Journal();
                }
                return _journal;
            }
            // Used by the getter; also by RootBrowserWindow and Frame to install a deserialized Journal.
            set
            {
                Debug.Assert(_journal == null && value != null,
                    "The Journal should be set only once and never removed."); // see bug 1367999
                _journal = value;
                _journal.Filter = new JournalEntryFilter(this.IsEntryNavigable);
                _journal.BackForwardStateChange += new EventHandler(OnBackForwardStateChange);

                DependencyObject navigator = (DependencyObject)_host;
                navigator.SetValue(BackStackPropertyKey, _journal.BackStack);
                navigator.SetValue(ForwardStackPropertyKey, _journal.ForwardStack);

                _host.OnJournalAvailable();
            }
        }

        internal NavigationService RootNavigationService
        {
            get { return _rootNavSvc; }
        }

        internal INavigatorBase NavigatorHost
        {
            get { return _host; }
        }

        #endregion Internal Properties

        //------------------------------------------------------
        //
        //  Private Methods
        //
        //------------------------------------------------------
        #region Private Methods

        private void OnBackForwardStateChange(object sender, EventArgs e)
        {
            Debug.Assert(sender == _journal);

            // Update CanGoBack and CanGoForward on the host navigator, only if actually changed.
            DependencyObject navigator = (DependencyObject)_host;
            bool canGoBackFwdChanged = false;
            bool newState = _journal.CanGoBack;
            if (newState != (bool)navigator.GetValue(CanGoBackProperty))
            {
                navigator.SetValue(CanGoBackPropertyKey, BooleanBoxes.Box(newState));
                canGoBackFwdChanged = true;
            }
            newState = _journal.CanGoForward;
            if (newState != (bool)navigator.GetValue(CanGoForwardProperty))
            {
                navigator.SetValue(CanGoForwardPropertyKey, BooleanBoxes.Box(newState));
                canGoBackFwdChanged = true;
            }

            if (canGoBackFwdChanged)
            {
                System.Windows.Input.CommandManager.InvalidateRequerySuggested();
            }
        }

        /// <summary>
        /// This is the filter callback for Journal. To keep a single code path and to avoid
        /// inconsistent results, use Journal.IsNavigable() instead of this method.
        /// </summary>
        private bool IsEntryNavigable(JournalEntry entry)
        {
            if (entry == null || !entry.IsNavigable())
                return false;
            // If the entry is associated with a child frame, the frame has to be currently available.
            // For a given journal entry group, only the "exit" entry is made visible. Effectively, 
            // this collapses all fragment-navigation and CustomContentState-navigation entries for
            // a page (other than the current one) to a single entry. That's what IE does.
            NavigationService ns = _rootNavSvc.FindTarget(entry.NavigationServiceId);
            return ns != null
                && (ns.ContentId == entry.ContentId || entry.JEGroupState.GroupExitEntry == entry);
        }

        private bool InAppShutdown
        {
            get { return System.Windows.Application.IsShuttingDown; }
        }

        #endregion

        //------------------------------------------------------    
        //    
        //  Private Fields    
        //    
        //------------------------------------------------------

        #region Private Fields

        private IJournalNavigationScopeHost _host;
        private NavigationService _rootNavSvc; // == _host.NavigationService
        private Journal _journal; // lazily-created; see Journal property

        #endregion Private Fields
    };


    /// <summary>
    /// The interface through which JournalNavigationScope talks back to its navigator host.
    /// </summary>
    internal interface IJournalNavigationScopeHost : INavigatorBase
    {
        NavigationService NavigationService { get; }

        void VerifyContextAndObjectState();

        void OnJournalAvailable();

        /// <summary>
        /// Allows the navigator host to do its own handling of GoBack. Currently used by 
        /// RootBrowserWindow to do the TravelLog integration.
        /// </summary>
        /// <returns> True to cancel the normal handling of GoBack </returns>
        bool GoBackOverride();
        bool GoForwardOverride();
    };
}
