// Copyright (C) 2003, 2005 Daisuke Arai <darai@users.sourceforge.jp>
// Copyright (C) 2008, 2013 panacoran <panacoran@users.sourceforge.jp>
// 
// This program is part of Protra.
//
// Protra is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, see <http://www.gnu.org/licenses/>.
// 
// $Id: Value.cs 456 2013-06-11 01:04:38Z panacoran $

using System;

namespace Protra.Lib.Lang
{
    /// <summary>
    /// プログラムで扱う値を統一的に表現するためのクラスです。
    /// </summary>
    [Serializable]
    public class Value : IComparable
    {
        /// <summary>
        /// 値の型を表す列挙型です。
        /// </summary>
        public enum Type : byte
        {
            /// <summary>
            /// int
            /// </summary>
            Int,

            /// <summary>
            /// float
            /// </summary>
            Float,

            /// <summary>
            /// string
            /// </summary>
            String,

            /// <summary>
            /// array
            /// </summary>
            Array
        }

        /// <summary>
        /// 値の実体
        /// </summary>
        private readonly object _value;

        /// <summary>
        /// 値の型
        /// </summary>
        private readonly Type _type;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="value">整数値</param>
        public Value(int value)
        {
            _value = value;
            _type = Type.Int;
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="value">実数値</param>
        public Value(double value)
        {
            _value = value;
            _type = Type.Float;
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="value">論理値</param>
        public Value(bool value)
        {
            _value = value ? 1 : 0;
            _type = Type.Int;
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="value">文字列</param>
        public Value(string value)
        {
            _value = value;
            _type = Type.String;
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="value">配列</param>
        public Value(Value[] value)
        {
            _value = value;
            _type = Type.Array;
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="value">値</param>
        public Value(Value value)
        {
            _value = value._value;
            _type = value._type;
        }

        /// <summary>
        /// 内部的値の実体を取得します。
        /// </summary>
        public object InnerValue
        {
            get { return _value; }
        }

        /// <summary>
        /// 型を示す値を取得します。
        /// </summary>
        public Type ValueType
        {
            get { return _type; }
        }

        /// <summary>
        /// 値がtrueと評価されるかどうかを示す論理値を取得します。
        /// 0はfalse、それ以外はtrueになります。
        /// </summary>
        public bool IsTrue
        {
            get { return !IsFalse; }
        }

        /// <summary>
        /// 値がfalseと評価されるかどうかを示す論理値を取得します。
        /// 0はfalse、それ以外はtrueになります。
        /// </summary>
        public bool IsFalse
        {
            get
            {
                return _type == Type.Int && (int)_value == 0 ||
                       _type == Type.Float && FloatComparers.Equal((double)_value, 0.0);
            }
        }

        /// <summary>
        /// 配列の要素を取得または設定します。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        public Value this[Value index]
        {
            get
            {
                if (index == null)
                    throw new RuntimeException("assigned null for index", null);
                if (_type != Type.Array)
                    throw new RuntimeException("assigned index for non-array", null);
                if (index._type != Type.Int)
                    throw new RuntimeException("assigned non-int value for array index", null);
                try
                {
                    return ((Value[])_value)[(int)index._value];
                }
                catch (IndexOutOfRangeException)
                {
                    throw new RuntimeException("array index out of range", null);
                }
            }

            set
            {
                if (index == null)
                    throw new RuntimeException("assigned null for index", null);
                if (_type != Type.Array)
                    throw new RuntimeException("assigned index for non-array", null);
                if (index._type != Type.Int)
                    throw new RuntimeException("assigned non-int value for array index", null);
                try
                {
                    ((Value[])_value)[(int)index._value] = value;
                }
                catch (IndexOutOfRangeException)
                {
                    throw new RuntimeException("array index out of range", null);
                }
            }
        }

        /// <summary>
        /// 指定された型にキャストします。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        /// <param name="target">型</param>
        /// <returns>キャスト結果です。</returns>
        public Value Cast(Type target)
        {
            if (_type == Type.Array)
                throw new RuntimeException("array casted", null);
            if (target == Type.String)
                return new Value(_value.ToString());
            switch (_type)
            {
                case Type.Int:
                    switch (target)
                    {
                        case Type.Int:
                            return new Value((int)_value);
                        case Type.Float:
                            return new Value((double)(int)_value);
                    }
                    break;
                case Type.Float:
                    switch (target)
                    {
                        case Type.Int:
                            return new Value((int)(double)_value);
                        case Type.Float:
                            return new Value((double)_value);
                    }
                    break;
                case Type.String:
                    switch (target)
                    {
                        case Type.Int:
                            try
                            {
                                return new Value(int.Parse((string)_value));
                            }
                            catch (FormatException)
                            {
                                return new Value(0);
                            }
                        case Type.Float:
                            try
                            {
                                return new Value(double.Parse((string)_value));
                            }
                            catch (FormatException)
                            {
                                return new Value(0.0);
                            }
                    }
                    break;
            }
            throw new RuntimeException("unexpected error", null);
        }

        /// <summary>
        /// 指定したオブジェクトが現在のオブジェクトと等しいかどうかを判断する。
        /// </summary>
        /// <param name="o">比較するオブジェクト。</param>
        /// <returns>等しい場合はtrue。それ以外の場合はfalse。</returns>
        public override bool Equals(object o)
        {
            var v = o as Value;
            if ((object)v == null)
                return false;
            switch (_type)
            {
                case Type.String:
                    return v._type == Type.String && (string)_value == (string)v._value;
                case Type.Array:
                    return v._type == Type.Array && _value == v._value;
                case Type.Int:
                    return v._type == Type.Int
                               ? (int)_value == (int)v._value
                               : FloatComparers.Equal((int)_value, (double)v._value);
                case Type.Float:
                    return v._type == Type.Int
                               ? FloatComparers.Equal((double)_value, (int)v._value)
                               : FloatComparers.Equal((double)_value, (double)v._value);
            }
            return false;
        }

        /// <summary>
        /// ハッシュ値を計算します。
        /// </summary>
        /// <returns>ハッシュ値</returns>
        public override int GetHashCode()
        {
            return _value.GetHashCode();
        }

        /// <summary>
        /// 現在のオブジェクトを指定したオブジェクトと比較する。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされる。
        /// </exception>
        /// <param name="o">比較するオブジェクト。</param>
        /// <returns>比較結果を表す整数。</returns>
        public int CompareTo(Object o)
        {
            var v = (Value)o;
            if (_type == Type.Array || v._type == Type.Array)
                throw new RuntimeException("array compared", null);
            switch (_type)
            {
                case Type.Int:
                    switch (v._type)
                    {
                        case Type.Int:
                            return (int)_value - (int)v._value;
                        case Type.Float:
                            return FloatComparers.Compare((int)_value, (double)v._value);
                        case Type.String:
                            throw new RuntimeException("int compared with string", null);
                    }
                    break;
                case Type.Float:
                    switch (v._type)
                    {
                        case Type.Int:
                            return FloatComparers.Compare((double)_value, (int)v._value);
                        case Type.Float:
                            return FloatComparers.Compare((double)_value, (double)v._value);
                        case Type.String:
                            throw new RuntimeException("float compared with string", null);
                    }
                    break;
                case Type.String:
                    switch (v._type)
                    {
                        case Type.Int:
                            throw new RuntimeException("string compared with int", null);
                        case Type.Float:
                            throw new RuntimeException("string compared with float", null);
                        case Type.String:
                            return ((string)_value).CompareTo(v._value);
                    }
                    break;
            }
            throw new RuntimeException("unexpected error", null);
        }

        /// <summary>
        /// 等価比較を行います。
        /// </summary>
        /// <param name="v1">オペランド1</param>
        /// <param name="v2">オペランド2</param>
        /// <returns>比較結果です。</returns>
        public static bool operator ==(Value v1, Value v2)
        {
            if ((object)v1 != null)
                return v1.Equals(v2);
            return (object)v2 == null;
        }

        /// <summary>
        /// 等価比較を行います。
        /// </summary>
        /// <param name="v1">オペランド1</param>
        /// <param name="v2">オペランド2</param>
        /// <returns>比較結果です。</returns>
        public static bool operator !=(Value v1, Value v2)
        {
            return !(v1 == v2);
        }

        /// <summary>
        /// 大小比較を行います。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        /// <param name="v1">オペランド1</param>
        /// <param name="v2">オペランド2</param>
        /// <returns>比較結果です。</returns>
        public static bool operator <(Value v1, Value v2)
        {
            if (v1 == null || v2 == null)
                throw new RuntimeException("null compared", null);
            return v1.CompareTo(v2) < 0;
        }

        /// <summary>
        /// 大小比較を行います。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        /// <param name="v1">オペランド1</param>
        /// <param name="v2">オペランド2</param>
        /// <returns>比較結果です。</returns>
        public static bool operator >(Value v1, Value v2)
        {
            if (v1 == null || v2 == null)
                throw new RuntimeException("null compared", null);
            return v1.CompareTo(v2) > 0;
        }

        /// <summary>
        /// 大小比較を行います。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        /// <param name="v1">オペランド1</param>
        /// <param name="v2">オペランド2</param>
        /// <returns>比較結果です。</returns>
        public static bool operator <=(Value v1, Value v2)
        {
            if (v1 == null || v2 == null)
                throw new RuntimeException("null compared", null);
            return v1.CompareTo(v2) <= 0;
        }

        /// <summary>
        /// 大小比較を行います。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        /// <param name="v1">オペランド1</param>
        /// <param name="v2">オペランド2</param>
        /// <returns>比較結果です。</returns>
        public static bool operator >=(Value v1, Value v2)
        {
            if (v1 == null || v2 == null)
                throw new RuntimeException("null compared", null);
            return v1.CompareTo(v2) >= 0;
        }

        /// <summary>
        /// 和を計算します。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        /// <param name="v1">オペランド1</param>
        /// <param name="v2">オペランド2</param>
        /// <returns>演算結果です。</returns>
        public static Value operator +(Value v1, Value v2)
        {
            if (v1 == null || v2 == null)
                throw new RuntimeException("binary operator '+' isn't defined for null", null);
            if (v1._type == Type.Array || v2._type == Type.Array)
                throw new RuntimeException("binary operator '+' isn't defined for array", null);
            switch (v1._type)
            {
                case Type.Int:
                    switch (v2._type)
                    {
                        case Type.Int:
                            return new Value((int)v1._value + (int)v2._value);
                        case Type.Float:
                            return new Value((int)v1._value + (double)v2._value);
                        case Type.String:
                            return new Value(v1._value + v2._value.ToString());
                    }
                    break;
                case Type.Float:
                    switch (v2._type)
                    {
                        case Type.Int:
                            return new Value((double)v1._value + (int)v2._value);
                        case Type.Float:
                            return new Value((double)v1._value + (double)v2._value);
                        case Type.String:
                            return new Value(v1._value + v2._value.ToString());
                    }
                    break;
                case Type.String:
                    return new Value(v1._value + v2._value.ToString());
            }

            throw new RuntimeException("unexpected error", null);
        }

        /// <summary>
        /// 差を計算します。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        /// <param name="v1">オペランド1</param>
        /// <param name="v2">オペランド2</param>
        /// <returns>演算結果です。</returns>
        public static Value operator -(Value v1, Value v2)
        {
            if (v1 == null || v2 == null)
                throw new RuntimeException("binary operator '-' isn't defined for null", null);
            if (v1._type == Type.String || v2._type == Type.String)
                throw new RuntimeException("binary operator '-' isn't defined for string", null);
            if (v1._type == Type.Array || v2._type == Type.Array)
                throw new RuntimeException("binary operator '-' isn't defined for array", null);
            switch (v1._type)
            {
                case Type.Int:
                    switch (v2._type)
                    {
                        case Type.Int:
                            return new Value((int)v1._value - (int)v2._value);
                        case Type.Float:
                            return new Value((int)v1._value - (double)v2._value);
                    }
                    break;
                case Type.Float:
                    switch (v2._type)
                    {
                        case Type.Int:
                            return new Value((double)v1._value - (int)v2._value);
                        case Type.Float:
                            return new Value((double)v1._value - (double)v2._value);
                    }
                    break;
            }

            throw new RuntimeException("unexpected error", null);
        }

        /// <summary>
        /// 積を計算します。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        /// <param name="v1">オペランド1</param>
        /// <param name="v2">オペランド2</param>
        /// <returns>演算結果です。</returns>
        public static Value operator *(Value v1, Value v2)
        {
            if (v1 == null || v2 == null)
                throw new RuntimeException("binary operator '*' isn't defined for null", null);
            if (v1._type == Type.String || v2._type == Type.String)
                throw new RuntimeException("binary operator '*' isn't defined for string", null);
            if (v1._type == Type.Array || v2._type == Type.Array)
                throw new RuntimeException("binary operator '*' isn't defined for array", null);
            switch (v1._type)
            {
                case Type.Int:
                    switch (v2._type)
                    {
                        case Type.Int:
                            return new Value((int)v1._value * (int)v2._value);
                        case Type.Float:
                            return new Value((int)v1._value * (double)v2._value);
                    }
                    break;
                case Type.Float:
                    switch (v2._type)
                    {
                        case Type.Int:
                            return new Value((double)v1._value * (int)v2._value);
                        case Type.Float:
                            return new Value((double)v1._value * (double)v2._value);
                    }
                    break;
            }

            throw new RuntimeException("unexpected error", null);
        }

        /// <summary>
        /// 商を計算します。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        /// <param name="v1">オペランド1</param>
        /// <param name="v2">オペランド2</param>
        /// <returns>演算結果です。</returns>
        public static Value operator /(Value v1, Value v2)
        {
            if (v1 == null || v2 == null)
                throw new RuntimeException("binary operator '/' isn't defined for null", null);
            if (v1._type == Type.String || v2._type == Type.String)
                throw new RuntimeException("binary operator '/' isn't defined for string", null);
            if (v1._type == Type.Array || v2._type == Type.Array)
                throw new RuntimeException("binary operator '/' isn't defined for array", null);
            if (v2._type == Type.Int && (int)v2._value == 0 ||
                v2._type == Type.Float && FloatComparers.Equal((double)v2._value, 0.0))
                throw new RuntimeException("divided by 0", null);
            switch (v1._type)
            {
                case Type.Int:
                    switch (v2._type)
                    {
                        case Type.Int:
                            return new Value((int)v1._value / (int)v2._value);
                        case Type.Float:
                            return new Value((int)v1._value / (double)v2._value);
                    }
                    break;
                case Type.Float:
                    switch (v2._type)
                    {
                        case Type.Int:
                            return new Value((double)v1._value / (int)v2._value);
                        case Type.Float:
                            return new Value((double)v1._value / (double)v2._value);
                    }
                    break;
            }
            throw new RuntimeException("unexpected error", null);
        }

        /// <summary>
        /// 余りを計算します。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        /// <param name="v1">オペランド1</param>
        /// <param name="v2">オペランド2</param>
        /// <returns>演算結果です。</returns>
        public static Value operator %(Value v1, Value v2)
        {
            if (v1 == null || v2 == null)
                throw new RuntimeException("binary operator '%' isn't defined for null", null);
            if (v1._type == Type.Float || v2._type == Type.Float)
                throw new RuntimeException("binary operator '%' isn't defined for float", null);
            if (v1._type == Type.String || v2._type == Type.String)
                throw new RuntimeException("binary operator '%' isn't defined for string", null);
            if (v1._type == Type.Array || v2._type == Type.Array)
                throw new RuntimeException("binary operator '%' isn't defined for array", null);
            switch (v1._type)
            {
                case Type.Int:
                    switch (v2._type)
                    {
                        case Type.Int:
                            if ((int)v2._value == 0)
                                throw new RuntimeException("divided by 0", null);
                            return new Value((int)v1._value % (int)v2._value);
                    }
                    break;
            }

            throw new RuntimeException("unexpected error", null);
        }

        /// <summary>
        /// 否定を計算します。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        /// <param name="v">オペランド</param>
        /// <returns>演算結果です。</returns>
        public static Value operator !(Value v)
        {
            return new Value(v == null || v.IsFalse);
        }

        /// <summary>
        /// プラス符号を計算します。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        /// <param name="v">オペランド</param>
        /// <returns>演算結果です。</returns>
        public static Value operator +(Value v)
        {
            switch (v._type)
            {
                case Type.Int:
                case Type.Float:
                    return new Value(v);
                case Type.String:
                    throw new RuntimeException("unary operator '+' isn't defined for string", null);
                case Type.Array:
                    throw new RuntimeException("unary operator '+' isn't defined for array", null);
            }

            throw new RuntimeException("unexpected error", null);
        }

        /// <summary>
        /// マイナス符号を計算します。
        /// </summary>
        /// <exception cref="Protra.Lib.Lang.RuntimeException">
        /// プログラム実行中にエラーが発生した場合にthrowされます。
        /// </exception>
        /// <param name="v">オペランド</param>
        /// <returns>演算結果です。</returns>
        public static Value operator -(Value v)
        {
            switch (v._type)
            {
                case Type.Int:
                    return new Value(-(int)v._value);
                case Type.Float:
                    return new Value(-(double)v._value);
                case Type.String:
                    throw new RuntimeException("unary operator '-' isn't defined for string", null);
                case Type.Array:
                    throw new RuntimeException("unary operator '-' isn't defined for array", null);
            }

            throw new RuntimeException("unexpected error", null);
        }
    }
}