using Lucene.Net.Benchmarks.ByTask.Stats;
using Lucene.Net.Benchmarks.ByTask.Utils;
using System;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Threading;
using Console = Lucene.Net.Util.SystemConsole;

namespace Lucene.Net.Benchmarks.ByTask.Tasks
{
    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF licenses this file to You under the Apache License, Version 2.0
     * (the "License"); you may not use this file except in compliance with
     * the License.  You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */

    /// <summary>
    /// An abstract task to be tested for performance.
    /// </summary>
    /// <remarks>
    /// Every performance task extends this class, and provides its own
    /// <see cref="DoLogic()"/> method, which performs the actual task.
    /// <para/>
    /// Tasks performing some work that should be measured for the task, can override
    /// <see cref="Setup()"/> and/or <see cref="TearDown()"/> and place that work there.
    /// <para/>
    /// Relevant properties:
    /// <list type="bullet">
    ///     <item><term>task.max.depth.log</term><description></description></item>
    /// </list>
    /// <para/>
    /// Also supports the following logging attributes:
    /// <list type="bullet">
    ///     <item><term>log.step</term><description>
    ///         specifies how often to log messages about the current running
    ///         task. Default is 1000 <see cref="DoLogic()"/> invocations. Set to -1 to disable
    ///         logging.
    ///     </description></item>
    ///     <item><term>log.step.[class Task Name]</term><description>
    ///         specifies the same as 'log.step', only for a
    ///         particular task name. For example, log.step.AddDoc will be applied only for
    ///         <see cref="AddDocTask"/>. It's a way to control
    ///         per task logging settings. If you want to omit logging for any other task,
    ///         include log.step=-1. The syntax is "log.step." together with the Task's
    ///         'short' name (i.e., without the 'Task' part).
    ///     </description></item>
    /// </list>
    /// </remarks>
    public abstract class PerfTask : IDisposable
#if FEATURE_CLONEABLE
        , System.ICloneable
#endif
    {
        internal static readonly int DEFAULT_LOG_STEP = 1000;

        private PerfRunData runData;

        // propeties that all tasks have
        private string name;
        private int depth = 0;
        protected int m_logStep;
        private int logStepCount = 0;
        private int maxDepthLogStart = 0;
        private bool disableCounting = false;
        protected string m_params = null;

        private bool runInBackground;
        private int deltaPri;

        // The first line of this task's definition in the alg file
        private int algLineNum = 0;

        protected static readonly string NEW_LINE = Environment.NewLine;

        /// <summary>
        /// Should not be used externally
        /// </summary>
        private PerfTask()
        {
            name = GetType().Name;
            if (name.EndsWith("Task", StringComparison.Ordinal))
            {
                name = name.Substring(0, name.Length - 4);
            }
        }

        public virtual void SetRunInBackground(int deltaPri)
        {
            runInBackground = true;
            this.deltaPri = deltaPri;
        }

        public virtual bool RunInBackground => runInBackground;

        public virtual int BackgroundDeltaPriority => deltaPri;

        // LUCENENET specific - made private and
        // added Stop property because volatile
        // fields cannot be protected.
        private volatile bool stopNow;

        protected bool Stop
        {
            get => stopNow;
            set => stopNow = value;
        }
        public virtual void StopNow()
        {
            stopNow = true;
        }

        public PerfTask(PerfRunData runData)
            : this()
        {
            this.runData = runData;
            Config config = runData.Config;
            this.maxDepthLogStart = config.Get("task.max.depth.log", 0);

            string logStepAtt = "log.step";
            string taskLogStepAtt = "log.step." + name;
            if (config.Get(taskLogStepAtt, null) != null)
            {
                logStepAtt = taskLogStepAtt;
            }

            // It's important to read this from Config, to support vals-by-round.
            m_logStep = config.Get(logStepAtt, DEFAULT_LOG_STEP);
            // To avoid the check 'if (logStep > 0)' in tearDown(). This effectively
            // turns logging off.
            if (m_logStep <= 0)
            {
                m_logStep = int.MaxValue;
            }
        }

        public virtual object Clone()
        {
            // tasks having non primitive data structures should override this.
            // otherwise parallel running of a task sequence might not run correctly. 
            return (PerfTask)base.MemberwiseClone();
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
        }

        /// <summary>
        /// Run the task, record statistics.
        /// </summary>
        /// <param name="reportStats"></param>
        /// <returns>Number of work items done by this task.</returns>
        public int RunAndMaybeStats(bool reportStats)
        {
            int count;
            if (!reportStats || ShouldNotRecordStats)
            {
                Setup();
                count = DoLogic();
                count = disableCounting ? 0 : count;
                TearDown();
                return count;
            }
            if (reportStats && depth <= maxDepthLogStart && !ShouldNeverLogAtStart)
            {
                Console.WriteLine("------------> starting task: " + GetName());
            }
            Setup();
            Points pnts = runData.Points;
            TaskStats ts = pnts.MarkTaskStart(this, runData.Config.RoundNumber);
            count = DoLogic();
            count = disableCounting ? 0 : count;
            pnts.MarkTaskEnd(ts, count);
            TearDown();
            return count;
        }

        /// <summary>
        /// Perform the task once (ignoring repetitions specification).
        /// Return number of work items done by this task.
        /// For indexing that can be number of docs added.
        /// For warming that can be number of scanned items, etc.
        /// </summary>
        /// <returns>Number of work items done by this task.</returns>
        public abstract int DoLogic();

        /// <summary>
        /// Returns the name.
        /// </summary>
        public virtual string GetName()
        {
            if (m_params == null)
            {
                return name;
            }
            return new StringBuilder(name).Append('(').Append(m_params).Append(')').ToString();
        }

        /// <summary>
        /// Sets the name.
        /// </summary>
        /// <param name="name">The name to set.</param>
        protected virtual void SetName(string name)
        {
            this.name = name;
        }

        /// <summary>
        /// Gets the run data.
        /// </summary>
        public virtual PerfRunData RunData => runData;

        /// <summary>
        /// Gets or Sets the depth.
        /// </summary>
        public virtual int Depth
        {
            get => depth;
            set => depth = value;
        }

        // compute a blank string padding for printing this task indented by its depth  
        internal string GetPadding()
        {
            char[] c = new char[4 * Depth];
            for (int i = 0; i < c.Length; i++) c[i] = ' ';
            return new string(c);
        }

        public override string ToString()
        {
            string padd = GetPadding();
            StringBuilder sb = new StringBuilder(padd);
            if (disableCounting)
            {
                sb.Append('-');
            }
            sb.Append(GetName());
            if (RunInBackground)
            {
                sb.Append(" &");
                int x = BackgroundDeltaPriority;
                if (x != 0)
                {
                    sb.Append(x);
                }
            }
            return sb.ToString();
        }

        /// <summary>
        /// Returns the maxDepthLogStart.
        /// </summary>
        internal int MaxDepthLogStart => maxDepthLogStart;

        protected virtual string GetLogMessage(int recsCount)
        {
            return "processed " + recsCount + " records";
        }

        /// <summary>
        /// Tasks that should never log at start can override this.
        /// Returns <c>true</c> if this task should never log when it start.
        /// </summary>
        protected virtual bool ShouldNeverLogAtStart => false;

        /// <summary>
        /// Tasks that should not record statistics can override this. 
        /// Returns <c>true</c> if this task should never record its statistics.
        /// </summary>
        protected virtual bool ShouldNotRecordStats => false;

        /// <summary>
        /// Task setup work that should not be measured for that specific task. By
        /// default it does nothing, but tasks can implement this, moving work from
        /// <see cref="DoLogic()"/> to this method. Only the work done in <see cref="DoLogic()"/>
        /// is measured for this task. Notice that higher level (sequence) tasks
        /// containing this task would then measure larger time than the sum of their
        /// contained tasks.
        /// </summary>
        public virtual void Setup()
        {
        }

        /// <summary>
        /// Task teardown work that should not be measured for that specific task. By
        /// default it does nothing, but tasks can implement this, moving work from
        /// <see cref="DoLogic()"/> to this method. Only the work done in <see cref="DoLogic()"/>
        /// is measured for this task. Notice that higher level (sequence) tasks
        /// containing this task would then measure larger time than the sum of their        
        /// contained tasks.
        /// </summary>
        public virtual void TearDown()
        {
            if (++logStepCount % m_logStep == 0)
            {
                double time = (((Stopwatch.GetTimestamp() / Stopwatch.Frequency) * 1000) - runData.StartTimeMillis) / 1000.0;
                Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0:0000000.00}", time) + " sec --> "
                    + Thread.CurrentThread.Name + " " + GetLogMessage(logStepCount));
            }
        }

        /// <summary>
        /// Sub classes that support parameters must override this method to return
        /// <c>true</c> if this task supports command line params.
        /// </summary>
        public virtual bool SupportsParams => false;

        /// <summary>
        /// Set the params of this task.
        /// </summary>
        /// <exception cref="NotSupportedException">For tasks supporting command line parameters.</exception>
        public virtual void SetParams(string @params)
        {
            if (!SupportsParams)
            {
                throw new NotSupportedException(GetName() + " does not support command line parameters.");
            }
            this.m_params = @params;
        }

        /// <summary>
        /// Gets the Params.
        /// </summary>
        public virtual string Params => m_params;

        /// <summary>
        /// Return <c>true</c> if counting is disabled for this task.
        /// </summary>
        public virtual bool DisableCounting
        {
            get => disableCounting;
            set => disableCounting = value;
        }

        public virtual int AlgLineNum
        {
            get => algLineNum;
            set => algLineNum = value;
        }
    }
}
