﻿namespace Dealership.Engine
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using Common.Enums;
    using Contracts;
    using Factories;

    public sealed class DealershipEngine : IEngine
    {
        // Commands constants (do not use these, they serve to the engine)
        private const string InvalidCommand = "Invalid command!";

        private const string UserAlreadyExist = "User {0} already exist. Choose a different username!";
        private const string UserLoggedInAlready = "User {0} is logged in! Please log out first!";
        private const string UserRegisterеd = "User {0} registered successfully!";
        private const string UserNotLogged = "You are not logged! Please login first!";
        private const string NoSuchUser = "There is no user with username {0}!";
        private const string UserLoggedOut = "You logged out!";
        private const string UserLoggedIn = "User {0} successfully logged in!";
        private const string WrongUsernameOrPassword = "Wrong username or password!";
        private const string YouAreNotAnAdmin = "You are not an admin!";

        private const string CommentAddedSuccessfully = "{0} added comment successfully!";
        private const string CommentRemovedSuccessfully = "{0} removed comment successfully!";

        private const string VehicleRemovedSuccessfully = "{0} removed vehicle successfully!";
        private const string VehicleAddedSuccessfully = "{0} added vehicle successfully!";

        private const string RemovedVehicleDoesNotExist = "Cannot remove comment! The vehicle does not exist!";
        private const string RemovedCommentDoesNotExist = "Cannot remove comment! The comment does not exist!";

        private const string CommentDoesNotExist = "The comment does not exist!";
        private const string VehicleDoesNotExist = "The vehicle does not exist!";

        private static readonly IEngine SingleInstance = new DealershipEngine();

        private readonly IDealershipFactory factory;
        private readonly ICollection<IUser> users;
        private IUser loggedUser;

        /// <summary>
        /// Initializes a new instance of the <seealso cref="DealershipEngine"/>
        /// </summary>
        private DealershipEngine()
        {
            this.factory = new DealershipFactory();
            this.users = new List<IUser>();
            this.loggedUser = null;
        }

        /// <summary>
        /// Getter for Instance.
        /// </summary>
        public static IEngine Instance
        {
            get { return SingleInstance; }
        }

        public void Start()
        {
            var commands = this.ReadCommands();
            var commandResult = this.ProcessCommands(commands);
            this.PrintReports(commandResult);
        }

        /// <summary>
        /// Engine for reading commands.
        /// </summary>
        /// <returns>New list with commands.</returns>
        private IList<ICommand> ReadCommands()
        {
            var commands = new List<ICommand>();

            var currentLine = Console.ReadLine();

            while (!string.IsNullOrEmpty(currentLine))
            {
                var currentCommand = new Command(currentLine);
                commands.Add(currentCommand);

                currentLine = Console.ReadLine();
            }

            return commands;
        }

        /// <summary>
        /// Process commands.
        /// </summary>
        /// <param name="commands"/>
        /// <returns>New list with processed commands.</returns>
        private IList<string> ProcessCommands(IList<ICommand> commands)
        {
            var reports = new List<string>();

            foreach (var command in commands)
            {
                try
                {
                    var report = this.ProcessSingleCommand(command);
                    reports.Add(report);
                }
                catch (Exception ex)
                {
                    reports.Add(ex.Message);
                }
            }

            return reports;
        }

        /// <summary>
        /// Print reports.
        /// </summary>
        /// <param name="reports"/>
        private void PrintReports(IList<string> reports)
        {
            var output = new StringBuilder();

            foreach (var report in reports)
            {
                output.AppendLine(report);
                output.AppendLine(new string('#', 20));
            }

            Console.Write(output.ToString());
        }

        /// <summary>
        /// Process single commands.
        /// </summary>
        /// <param name="command"></param>
        /// <returns></returns>
        private string ProcessSingleCommand(ICommand command)
        {
            if (command.Name != "RegisterUser" && command.Name != "Login")
            {
                if (this.loggedUser == null)
                {
                    return UserNotLogged;
                }
            }

            switch (command.Name)
            {
                case "RegisterUser":
                    var username = command.Parameters[0];
                    var firstName = command.Parameters[1];
                    var lastName = command.Parameters[2];
                    var password = command.Parameters[3];

                    var role = Role.Normal;

                    if (command.Parameters.Count > 4)
                    {
                        role = (Role)Enum.Parse(typeof(Role), command.Parameters[4]);
                    }

                    return this.RegisterUser(username, firstName, lastName, password, role);

                case "Login":
                    username = command.Parameters[0];
                    password = command.Parameters[1];

                    return this.Login(username, password);

                case "Logout":
                    return this.Logout();

                case "AddVehicle":
                    var type = command.Parameters[0];
                    var make = command.Parameters[1];
                    var model = command.Parameters[2];
                    var price = decimal.Parse(command.Parameters[3]);
                    var additionalParam = command.Parameters[4];
                    var typeEnum = (VehicleType)Enum.Parse(typeof(VehicleType), type, true);

                    return this.AddVehicle(typeEnum, make, model, price, additionalParam);

                case "RemoveVehicle":
                    var vehicleIndex = int.Parse(command.Parameters[0]) - 1;

                    return this.RemoveVehicle(vehicleIndex);

                case "AddComment":
                    var content = command.Parameters[0];
                    var author = command.Parameters[1];
                    vehicleIndex = int.Parse(command.Parameters[2]) - 1;

                    return this.AddComment(content, vehicleIndex, author);

                case "RemoveComment":
                    vehicleIndex = int.Parse(command.Parameters[0]) - 1;
                    var commentIndex = int.Parse(command.Parameters[1]) - 1;
                    username = command.Parameters[2];

                    return this.RemoveComment(vehicleIndex, commentIndex, username);

                case "ShowUsers":
                    return this.ShowAllUsers();

                case "ShowVehicles":
                    username = command.Parameters[0];

                    return this.ShowUserVehicles(username);

                default:
                    return string.Format(InvalidCommand, command.Name);
            }
        }

        /// <summary>
        /// Registers user, if there is no such user already.
        /// </summary>
        /// <param name="username">User username.</param>
        /// <param name="firstName">User first name.</param>
        /// <param name="lastName">User last name.</param>
        /// <param name="password">User password.</param>
        /// <param name="role">User role.</param>
        /// <returns><seealso cref="DealershipEngine"/> message for user.</returns>
        private string RegisterUser(string username, string firstName, string lastName, string password, Role role)
        {
            if (this.loggedUser != null)
            {
                return string.Format(UserLoggedInAlready, this.loggedUser.Username);
            }

            if (this.users.Any(u => u.Username.ToLower() == username.ToLower()))
            {
                return string.Format(UserAlreadyExist, username);
            }

            var user = this.factory.CreateUser(username, firstName, lastName, password, role.ToString());
            this.loggedUser = user;
            this.users.Add(user);

            return string.Format(UserRegisterеd, username);
        }

        /// <summary>
        /// Logs in user if there is no already logged in
        /// and there is such registered user.
        /// </summary>
        /// <param name="username">User username.</param>
        /// <param name="password">User password.</param>
        /// <returns><seealso cref="DealershipEngine"/> message for user.</returns>
        private string Login(string username, string password)
        {
            if (this.loggedUser != null)
            {
                return string.Format(UserLoggedInAlready, this.loggedUser.Username);
            }

            var userFound = this.users.FirstOrDefault(u => u.Username.ToLower() == username.ToLower());

            if (userFound != null && userFound.Password == password)
            {
                this.loggedUser = userFound;
                return string.Format(UserLoggedIn, username);
            }

            return WrongUsernameOrPassword;
        }

        /// <summary>
        /// Logs out the current logged in user.
        /// </summary>
        /// <returns>string: <seealso cref="UserLoggedOut"/></returns>
        private string Logout()
        {
            this.loggedUser = null;
            return UserLoggedOut;
        }

        /// <summary>
        /// Adds a vehicle to the current user.
        /// </summary>
        /// <param name="type">Vehicle type.</param>
        /// <param name="make">Vehicle make.</param>
        /// <param name="model">Vehicle model.</param>
        /// <param name="price">Vehicle price.</param>
        /// <param name="additionalParam">Depends on the type of the vehicle.</param>
        /// <returns>string: <seealso cref="VehicleAddedSuccessfully"/></returns>
        private string AddVehicle(VehicleType type, string make, string model, decimal price, string additionalParam)
        {
            IVehicle vehicle = null;

            switch (type)
            {
                case VehicleType.Car:
                    vehicle = this.factory.CreateCar(make, model, price, int.Parse(additionalParam));
                    break;
                case VehicleType.Motorcycle:
                    vehicle = this.factory.CreateMotorcycle(make, model, price, additionalParam);
                    break;
                case VehicleType.Truck:
                    vehicle = this.factory.CreateTruck(make, model, price, int.Parse(additionalParam));
                    break;
            }

            this.loggedUser.AddVehicle(vehicle);

            return string.Format(VehicleAddedSuccessfully, this.loggedUser.Username);
        }

        /// <summary>
        /// Remove the vehicle on that index if there is such.
        /// </summary>
        /// <param name="vehicleIndex">Vehicle index.</param>
        /// <returns>string: <seealso cref="VehicleRemovedSuccessfully"/></returns>
        private string RemoveVehicle(int vehicleIndex)
        {
            ValidateRange(vehicleIndex, 0, this.loggedUser.Vehicles.Count, RemovedVehicleDoesNotExist);

            var vehicle = this.loggedUser.Vehicles[vehicleIndex];

            this.loggedUser.RemoveVehicle(vehicle);

            return string.Format(VehicleRemovedSuccessfully, this.loggedUser.Username);
        }

        /// <summary>
        /// Add a comment with the content provided to the vehicle
        /// with that index and sets the author.
        /// </summary>
        /// <param name="content">Content.</param>
        /// <param name="vehicleIndex">Vehicle index.</param>
        /// <param name="author">Author.</param>
        /// <returns>string: <seealso cref="DealershipEngine"/></returns>
        private string AddComment(string content, int vehicleIndex, string author)
        {
            var comment = this.factory.CreateComment(content);
            comment.Author = this.loggedUser.Username;
            var user = this.users.FirstOrDefault(u => u.Username == author);

            if (user == null)
            {
                return string.Format(NoSuchUser, author);
            }

            ValidateRange(vehicleIndex, 0, user.Vehicles.Count, VehicleDoesNotExist);

            var vehicle = user.Vehicles[vehicleIndex];

            this.loggedUser.AddComment(comment, vehicle);

            return string.Format(CommentAddedSuccessfully, this.loggedUser.Username);
        }

        /// <summary>
        /// Removes the comment from the vehicle.
        /// </summary>
        /// <param name="vehicleIndex">Vehicle index.</param>
        /// <param name="commentIndex">Comment index.</param>
        /// <param name="username">Username.</param>
        /// <returns>string</returns>
        private string RemoveComment(int vehicleIndex, int commentIndex, string username)
        {
            var user = this.users.FirstOrDefault(u => u.Username == username);

            if (user == null)
            {
                return string.Format(NoSuchUser, username);
            }

            ValidateRange(vehicleIndex, 0, user.Vehicles.Count, RemovedVehicleDoesNotExist);
            ValidateRange(commentIndex, 0, user.Vehicles[vehicleIndex].Comments.Count, RemovedCommentDoesNotExist);

            var vehicle = user.Vehicles[vehicleIndex];
            var comment = user.Vehicles[vehicleIndex].Comments[commentIndex];

            this.loggedUser.RemoveComment(comment, vehicle);

            return string.Format(CommentRemovedSuccessfully, this.loggedUser.Username);
        }

        /// <summary>
        /// Shows all the users registerd.
        /// </summary>
        /// <returns></returns>
        private string ShowAllUsers()
        {
            if (this.loggedUser.Role != Role.Admin)
            {
                return YouAreNotAnAdmin;
            }

            var builder = new StringBuilder();
            builder.AppendLine("--USERS--");
            var counter = 1;

            foreach (var user in this.users)
            {
                builder.AppendLine(string.Format("{0}. {1}", counter, user.ToString()));
                counter++;
            }

            return builder
                .ToString()
                .Trim();
        }

        /// <summary>
        /// Shows all the vehicles of the user.
        /// </summary>
        /// <param name="username">User username.</param>
        /// <returns>string.</returns>
        private string ShowUserVehicles(string username)
        {
            var user = this.users.FirstOrDefault(u => u.Username.ToLower() == username.ToLower());

            if (user == null)
            {
                return string.Format(NoSuchUser, username);
            }

            return user.PrintVehicles();
        }

        /// <summary>
        /// Validatior for range.
        /// </summary>
        /// <param name="value">Value for validate.</param>
        /// <param name="min">Minimum range.</param>
        /// <param name="max">Maximum range.</param>
        /// <param name="message">Exception message.</param>
        /// <exception cref="ArgumentException"/>
        private static void ValidateRange(int? value, int min, int max, string message)
        {
            if (value < min || value >= max)
            {
                throw new ArgumentException(message);
            }
        }
    }
}
