using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace LibMpc
{
    /// 
    /// Keeps the connection to the MPD server and handels the most basic structure of the
    /// MPD protocol. The high level commands are handeled in the 
    /// class.
    /// 
    public class MpcConnection
    {
        private static readonly string FIRST_LINE_PREFIX = "OK MPD ";
        private static readonly string OK = "OK";
        private static readonly string ACK = "ACK";
        private readonly IPEndPoint _server;
        private TcpClient _tcpClient;
        private NetworkStream _networkStream;
        private StreamReader _reader;
        private StreamWriter _writer;
        private string _version;
        public MpcConnection(IPEndPoint server)
        {
            if (IsConnected) return;
            ClearConnectionFields();
            _server = server;
        }
        public bool IsConnected => (_tcpClient != null) && _tcpClient.Connected;
        public string Version => _version;
        public async Task ConnectAsync()
        {
            if (_server == null)
                throw new InvalidOperationException("Server IPEndPoint not set.");
            if (IsConnected)
                throw new AlreadyConnectedException();
            _tcpClient = new TcpClient();
            await _tcpClient.ConnectAsync(_server.Address, _server.Port);
            _networkStream = _tcpClient.GetStream();
            _reader = new StreamReader(_networkStream, Encoding.UTF8);
            _writer = new StreamWriter(_networkStream, Encoding.UTF8) { NewLine = "\n" };
            var firstLine = _reader.ReadLine();
            if (!firstLine.StartsWith(FIRST_LINE_PREFIX))
            {
                await DisconnectAsync();
                throw new InvalidDataException("Response of mpd does not start with \"" + FIRST_LINE_PREFIX + "\"." );
            }
            _version = firstLine.Substring(FIRST_LINE_PREFIX.Length);
            await _writer.WriteLineAsync();
            _writer.Flush();
            await ReadResponseAsync();
        }
        
        public Task DisconnectAsync()
        {
            if (_tcpClient == null)
            {
                return Task.CompletedTask;
            }
            _networkStream.Dispose();
            ClearConnectionFields();
            return Task.CompletedTask;
        }
        /// 
        /// Executes a simple command without arguments on the MPD server and returns the response.
        /// 
        /// The command to execute.
        /// The MPD server response parsed into a basic object.
        /// If the command contains a space of a newline charakter.
        public async Task Exec(string command)
        {
            if (command == null)
                throw new ArgumentNullException("command");
            if (command.Contains(" "))
                throw new ArgumentException("command contains space");
            if (command.Contains("\n"))
                throw new ArgumentException("command contains newline");
            // TODO: Integrate connection status in MpdResponse
            var connectionResult = await CheckConnectionAsync();
            try
            {
                _writer.WriteLine(command);
                _writer.Flush();
                return await ReadResponseAsync();
            }
            catch (Exception)
            {
                try { await DisconnectAsync(); } catch (Exception) { }
                return null; // TODO: Create Null Object for MpdResponse
            }
        }
        /// 
        /// Executes a MPD command with arguments on the MPD server.
        /// 
        /// The command to execute.
        /// The arguments of the command.
        /// The MPD server response parsed into a basic object.
        /// If the command contains a space of a newline charakter.
        public async Task Exec(string command, string[] argument)
        {
            if (command == null)
                throw new ArgumentNullException("command");
            if (command.Contains(" "))
                throw new ArgumentException("command contains space");
            if (command.Contains("\n"))
                throw new ArgumentException("command contains newline");
            if (argument == null)
                throw new ArgumentNullException("argument");
            for (int i = 0; i < argument.Length; i++)
            {
                if (argument[i] == null)
                    throw new ArgumentNullException("argument[" + i + "]");
                if (argument[i].Contains("\n"))
                    throw new ArgumentException("argument[" + i + "] contains newline");
            }
            // TODO: Integrate connection status in MpdResponse
            var connectionResult = await CheckConnectionAsync();
            try
            {
                _writer.Write(command);
                foreach (string arg in argument)
                {
                    _writer.Write(' ');
                    WriteToken(arg);
                }
                _writer.WriteLine();
                _writer.Flush();
                return await ReadResponseAsync();
            }
            catch (Exception)
            {
                try { await DisconnectAsync(); } catch (Exception) { }
                throw;
            }
        }
        private async Task CheckConnectionAsync()
        {
            if (!IsConnected)
            {
                await ConnectAsync();
            }
            return IsConnected;
        }
        private void WriteToken(string token)
        {
            if (token.Contains(" "))
            {
                _writer.Write("\"");
                foreach (char chr in token)
                    if (chr == '"')
                        _writer.Write("\\\"");
                    else
                        _writer.Write(chr);
            }
            else
                _writer.Write(token);
        }
        private async Task ReadResponseAsync()
        {
            var response = new List();
            var currentLine = _reader.ReadLine();
            // Read response untli reach end token (OK or ACK)
            while (!(currentLine.Equals(OK) || currentLine.StartsWith(ACK)))
            {
                response.Add(currentLine);
                currentLine = await _reader.ReadLineAsync();
            }
            if (currentLine.Equals(OK))
            {
                return new MpdResponse(new ReadOnlyCollection(response));
            }
            else
            {
                return AcknowledgeResponse.Parse(currentLine, response);
            }
        }
        private void ClearConnectionFields() 
        {
            _writer?.Dispose();
            _reader?.Dispose();
            _networkStream?.Dispose();
            _tcpClient?.Dispose();
            _version = string.Empty;
        }
    }
    public class AcknowledgeResponse
    {
        private static readonly Regex AcknowledgePattern = new Regex("^ACK \\[(?[0-9]*)@(?[0-9]*)] \\{(?[a-z]*)} (?.*)$");
        public static MpdResponse Parse(string acknowledgeResponse, IList payload)
        {
            var match = AcknowledgePattern.Match(acknowledgeResponse);
            if (match.Groups.Count != 5)
                throw new InvalidDataException("Error response not as expected");
            return new MpdResponse(
                int.Parse(match.Result("${code}")),
                int.Parse(match.Result("${nr}")),
                match.Result("${command}"),
                match.Result("${message}"),
                new ReadOnlyCollection(payload));
        }
    }
}