//
// NtpqPeerMapper.cs
//
// Author:
//       Carsten Sonne Larsen <cs@innolan.dk>
//
// Copyright (c) 2013 Carsten Sonne Larsen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using Ntp.Analyzer.Log;
using Ntp.Analyzer.Objects.Live;
using Ntp.Data;
using Ntp.System;

namespace Ntp.Analyzer.Data.Live
{
    /// <summary>
    /// Ntpq mapper parses status importLines from ntpq into objects.
    /// </summary>
    public sealed class NtpqPeerMapper : DataMapper<AssociationEntry>
    {
        public NtpqPeerMapper (FileInfo file, LogBase log)
            : base(log)
        {
            this.file = file;
        }

        public NtpqPeerMapper (ServerName server, LogBase log)
            : base(log)
        {
            this.server = server;
        }

        public NtpqPeerMapper (string[] lines, LogBase log)
            : base(log)
        {
            this.lines = lines;
        }

        private readonly FileInfo file;
        private readonly string[] lines;
        private readonly ServerName server;
        private readonly Object locker = new Object ();
        private List<AssociationEntry> entries;

        /// <summary>
        /// Read all data from ntpq in a sequential manner.
        /// </summary>
        /// <returns>The enumerator.</returns>
        public override IEnumerator<AssociationEntry> GetEnumerator ()
        {
            lock (locker) {
                Import ();

                return entries.GetEnumerator ();
            }
        }

        private void Import ()
        {
            if (entries != null)
                return;

            entries = new List<AssociationEntry> ();

            if (file != null)
                Import (file);
            else if (server != null)
                Import (server);
            else
                Import (lines);
        }

        /// <summary>
        /// Import the specified importLines.
        /// </summary>
        /// <param name='importLines'>
        /// Lines to import.
        /// </param>
        private void Import (IEnumerable<String> importLines)
        {
            List<string> linesToParse = new List<String> (importLines);

            linesToParse.RemoveRange (0, 2);

            foreach (string line in linesToParse) {
                AssociationEntry entry = ParseLine (line);

                entries.Add (entry);
            }
        }

        /// <summary>
        /// Import the specified importFile.
        /// </summary>
        /// <param name="importFile">The import file.</param>
        /// <exception cref="System.ArgumentException">No filename specified for import importFile.</exception>
        private void Import (FileInfo importFile)
        {
            if (importFile == null)
                throw new ArgumentException ("No filename specified for import importFile.");

            ReadFromStream (new StreamReader (importFile.OpenRead ()));
        }

        /// <summary>
        /// Import data from the specified importServer.
        /// </summary>
        /// <param name="importServer">Server to import from.</param>
        private void Import (ServerName importServer)
        {
            ShellCommand command = new ShellCommand (
                                       "ntpq",
                                       "-np " + importServer,
                                       Log) {
                ErrorMessage = "Unexpected error while reading peer stats"
            };

            StreamReader reader = command.Execute ();
            if (reader == null)
                return;

            ReadFromStream (reader);
        }

        private void ReadFromStream (StreamReader reader)
        {
            // Skip header
            reader.ReadLine ();
            reader.ReadLine ();

            while (reader.Peek () != -1) {
                string line = reader.ReadLine ();

                if (line == null || line.Trim () == String.Empty)
                    continue;

                AssociationEntry entry = ParseLine (line);

                entries.Add (entry);
            }
        }

        /// <summary>
        /// Parses the line.
        /// </summary>
        /// <returns>
        /// The line parsed to an object.
        /// </returns>
        /// <param name='line'>
        /// Line to parse.
        /// </param>
        /// <example>
        /// 1         2         3         4         5         6         7
        /// 01234567890123456789012345678901234567890123456789012345678901234567890123456789
        /// remote           refid      st t when poll reach   delay   offset  jitter
        /// ==============================================================================
        /// #guard.net.innom 193.190.230.66   2 u  956 1024  376    1.753    0.603   0.450
        /// </example>
        private AssociationEntry ParseLine (string line)
        {
            char state = line [0];
            string remote = line.Substring (1, 16).Trim ();
            string refid = line.Substring (17, 16).Trim ();
            int stratus = Convert.ToInt32 (line.Substring (33, 2).Trim ());
            char t = line [36];
            int lastPoll = CalcLastPoll (line);
            int pollFrequency = Convert.ToInt32 (line.Substring (43, 4).Trim ());
            int reach = Convert.ToInt32 (line.Substring (49, 3).Trim ());
            double delay = Convert.ToDouble (line.Substring (53, 8).Trim ());
            double offset = Convert.ToDouble (line.Substring (61, 9).Trim ());
            double jitter = Convert.ToDouble (line.Substring (70, 8).Trim ());

            AssociationEntry entry = new AssociationEntry (
                state, remote, refid, stratus, t,
                lastPoll, pollFrequency, reach, delay, offset, jitter);

            return entry;
        }

        /// <summary>
        /// Calculates the value of the last poll.
        /// </summary>
        /// <returns>
        /// The last poll.
        /// </returns>
        /// <param name='line'>
        /// Line to parse value from.
        /// </param>
        private int CalcLastPoll (string line)
        {
            string lastPollString = line.Substring (38, 4);

            char lastPollUnit = lastPollString.Substring (lastPollString.Length - 1, 1) [0];
            string lastPollValue = lastPollString.Substring (0, lastPollString.Length - 1).Trim ();

            switch (lastPollUnit) {
            case 'm':
                return Convert.ToInt32 (lastPollValue) * 60;
            case 'h':
                return Convert.ToInt32 (lastPollValue) * 60 * 60;
            case 'd':
                return Convert.ToInt32 (lastPollValue) * 60 * 60 * 24;
            case '-':
                return -1;
            default:
                return Convert.ToInt32 (lastPollString);
            }
        }
    }
}