using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;

namespace StardewModdingAPI
{
    /// <summary>A translation string with a fluent API to customise it.</summary>
    public class Translation
    {
        /*********
        ** Properties
        *********/
        /// <summary>The placeholder text when the translation is <c>null</c> or empty, where <c>{0}</c> is the translation key.</summary>
        internal const string PlaceholderText = "(no translation:{0})";

        /// <summary>The name of the relevant mod for error messages.</summary>
        private readonly string ModName;

        /// <summary>The locale for which the translation was fetched.</summary>
        private readonly string Locale;

        /// <summary>The underlying translation text.</summary>
        private readonly string Text;

        /// <summary>The value to return if the translations is undefined.</summary>
        private readonly string Placeholder;


        /*********
        ** Accessors
        *********/
        /// <summary>The original translation key.</summary>
        public string Key { get; }


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an isntance.</summary>
        /// <param name="modName">The name of the relevant mod for error messages.</param>
        /// <param name="locale">The locale for which the translation was fetched.</param>
        /// <param name="key">The translation key.</param>
        /// <param name="text">The underlying translation text.</param>
        internal Translation(string modName, string locale, string key, string text)
            : this(modName, locale, key, text, string.Format(Translation.PlaceholderText, key)) { }

        /// <summary>Construct an isntance.</summary>
        /// <param name="modName">The name of the relevant mod for error messages.</param>
        /// <param name="locale">The locale for which the translation was fetched.</param>
        /// <param name="key">The translation key.</param>
        /// <param name="text">The underlying translation text.</param>
        /// <param name="placeholder">The value to return if the translations is undefined.</param>
        internal Translation(string modName, string locale, string key, string text, string placeholder)
        {
            this.ModName = modName;
            this.Locale = locale;
            this.Key = key;
            this.Text = text;
            this.Placeholder = placeholder;
        }

        /// <summary>Throw an exception if the translation text is <c>null</c> or empty.</summary>
        /// <exception cref="KeyNotFoundException">There's no available translation matching the requested key and locale.</exception>
        public Translation Assert()
        {
            if (!this.HasValue())
                throw new KeyNotFoundException($"The '{this.ModName}' mod doesn't have a translation with key '{this.Key}' for the '{this.Locale}' locale or its fallbacks.");
            return this;
        }

        /// <summary>Replace the text if it's <c>null</c> or empty. If you set a <c>null</c> or empty value, the translation will show the fallback "no translation" placeholder (see <see cref="UsePlaceholder"/> if you want to disable that). Returns a new instance if changed.</summary>
        /// <param name="default">The default value.</param>
        public Translation Default(string @default)
        {
            return this.HasValue()
                ? this
                : new Translation(this.ModName, this.Locale, this.Key, @default);
        }

        /// <summary>Whether to return a "no translation" placeholder if the translation is <c>null</c> or empty. Returns a new instance.</summary>
        /// <param name="use">Whether to return a placeholder.</param>
        public Translation UsePlaceholder(bool use)
        {
            return new Translation(this.ModName, this.Locale, this.Key, this.Text, use ? string.Format(Translation.PlaceholderText, this.Key) : null);
        }

        /// <summary>Replace tokens in the text like <c>{{value}}</c> with the given values. Returns a new instance.</summary>
        /// <param name="tokens">An object containing token key/value pairs. This can be an anonymous object (like <c>new { value = 42, name = "Cranberries" }</c>), a dictionary, or a class instance.</param>
        /// <exception cref="ArgumentNullException">The <paramref name="tokens"/> argument is <c>null</c>.</exception>
        public Translation Tokens(object tokens)
        {
            if (string.IsNullOrWhiteSpace(this.Text) || tokens == null)
                return this;

            // get dictionary of tokens
            IDictionary<string, string> tokenLookup = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
            {
                // from dictionary
                if (tokens is IDictionary inputLookup)
                {
                    foreach (DictionaryEntry entry in inputLookup)
                    {
                        string key = entry.Key?.ToString().Trim();
                        if (key != null)
                            tokenLookup[key] = entry.Value?.ToString();
                    }
                }

                // from object properties
                else
                {
                    Type type = tokens.GetType();
                    foreach (PropertyInfo prop in type.GetProperties())
                        tokenLookup[prop.Name] = prop.GetValue(tokens)?.ToString();
                    foreach (FieldInfo field in type.GetFields())
                        tokenLookup[field.Name] = field.GetValue(tokens)?.ToString();
                }
            }

            // format translation
            string text = Regex.Replace(this.Text, @"{{([ \w\.\-]+)}}", match =>
            {
                string key = match.Groups[1].Value.Trim();
                return tokenLookup.TryGetValue(key, out string value)
                    ? value
                    : match.Value;
            });
            return new Translation(this.ModName, this.Locale, this.Key, text);
        }

        /// <summary>Get whether the translation has a defined value.</summary>
        public bool HasValue()
        {
            return !string.IsNullOrEmpty(this.Text);
        }

        /// <summary>Get the translation text. Calling this method isn't strictly necessary, since you can assign a <see cref="Translation"/> value directly to a string.</summary>
        public override string ToString()
        {
            return this.Placeholder != null && !this.HasValue()
                ? this.Placeholder
                : this.Text;
        }

        /// <summary>Get a string representation of the given translation.</summary>
        /// <param name="translation">The translation key.</param>
        public static implicit operator string(Translation translation)
        {
            return translation?.ToString();
        }
    }
}