using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using Ruby;
using StarEngine.Core;

namespace StarEngine.Ruby
{
    using VALUE = System.Int32;

    /// <summary>
    /// Ruby のインタプリタをカプセル化します。
    /// </summary>
    public partial class RubyInterpreter : IDisposable
    {
        public static RubyInterpreter Create(IGameEnvironment env)
        {
            if (Instance != null)
                throw new InvalidOperationException("RubyInterpreter オブジェクトは同時に二つ生成できません。");
            
            return Instance = new RubyInterpreter(env);
        }
        private static RubyInterpreter Instance;

        private RubyGame Game;
        private IGameEnvironment GameEnvironment;
        private ConsoleForm ConsoleForm;

        private RubyInterpreter(IGameEnvironment gameEnvironment)
        {
            this.Game = new RubyGame(this);
            this.GameEnvironment = gameEnvironment;
            this.ConsoleForm = new ConsoleForm();

            RUBY.ruby_init();
            RUBY.rb_eval_string("$KCODE = 'u'");

            RUBY.rb_define_singleton_method(RUBY.rb_eval_string("STDOUT"), "write", this.rb_stdout_write);
            RUBY.rb_define_singleton_method(RUBY.rb_eval_string("STDERR"), "write", this.rb_stdout_write);

            RUBY.rb_define_module_function(SERubyConsts.mStarEngine, "activate_console", this.mStarEngine_activate_console);

            this.EvaluateInnerFile("star_engine.rb");

            this.InitializeAudioModule();
            this.InitializeCSharpCachedObjectModule();
            this.InitializeGameModule();
            this.InitializeInputModule();
            this.InitializeScreenModule();

            this.InitializeFontClass();
            this.InitializeTextureClass();
        }

        private VALUE rb_stdout_write(VALUE self, VALUE str)
        {
            if (!this.ConsoleForm.Visible)
                this.ConsoleForm.Show();
            this.ConsoleForm.Write(RUBY.StringValuePtr(str));
            return RUBY.Qnil;
        }

        private VALUE mStarEngine_activate_console(VALUE self)
        {
            this.ConsoleForm.Show();
            this.ConsoleForm.Activate();
            Application.Run(this.ConsoleForm);
            return RUBY.Qnil;
        }

        /// <summary>
        /// ロードパスを追加します。
        /// </summary>
        /// <param name="path">ロードパス。</param>
        public void AddLoadPath(string path)
        {
            path = path.Replace('\\', '/');
            // UTF-8 だと、うまくディレクトリが扱えない (可能性がある)
            RUBY.rb_eval_string(Encoding.Default.GetBytes("$LOAD_PATH << '" + path + "'"));
        }

        public bool IsDisposed
        {
            get { return this.isDisposed; }
            private set { this.isDisposed = value; }
        }
        private bool isDisposed = false;

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        ~RubyInterpreter()
        {
            this.Dispose(false);
        }

        private void Dispose(bool isDisposing)
        {
            if (!this.IsDisposed)
            {
                this.IsDisposed = true;
                if (isDisposing)
                {
                    this.Cache.Clear();
                    this.ConsoleForm.Dispose();
                }
            }
        }

        /// <summary>
        /// スクリプトを評価します。
        /// </summary>
        /// <param name="bytes">Ruby スクリプト。</param>
        public void Evaluate(byte[] bytes)
        {
            int state = 0;
            RUBY.rb_eval_string_protect(bytes, ref state);
            if (state != 0)
                throw new RubyInterpreterException(this.Error + "\n" + this.ErrorBacktrace);
        }

        /// <summary>
        /// スクリプトを評価します。
        /// </summary>
        /// <param name="script">Ruby スクリプトの文字列。</param>
        public void Evaluate(string script)
        {
            this.Evaluate(RUBY.Encoding.GetBytes(script));
        }

        private void EvaluateInnerFile(string fileName)
        {
            Assembly asm = Assembly.GetExecutingAssembly();
            string scriptPrefix = asm.GetName().Name + ".Scripts.star_engine.";
            byte[] bytes;
            using (Stream stream = asm.GetManifestResourceStream(scriptPrefix + fileName))
            {
                bytes = new byte[stream.Length];
                stream.Read(bytes, 0, bytes.Length);
            }
            this.Evaluate(bytes);
        }

        /// <summary>
        /// 現在発生しているエラーを取得します。
        /// </summary>
        private string Error
        {
            get
            {
                return RUBY.StringValuePtr(RUBY.rb_eval_string("$!.to_s"));
            }
        }

        /// <summary>
        /// 現在発生しているエラーのバックトレースを取得します。
        /// </summary>
        private string ErrorBacktrace
        {
            get
            {
                return RUBY.StringValuePtr(RUBY.rb_eval_string("$!.backtrace.join(\"\\n\")"));
            }
        }

        private Dictionary<int, object> Cache = new Dictionary<int, object>();

        /// <summary>
        /// Ruby オブジェクトと C# オブジェクトとを関連付けます。
        /// </summary>
        /// <param name="rbObject">Ruby オブジェクト。</param>
        /// <param name="csObject">C# オブジェクト。</param>
        internal void AddCachedObject(VALUE rbObject, object csObject)
        {
            this.Cache.Add(RUBY.GetID(rbObject), csObject);
        }

        /// <summary>
        /// Ruby オブジェクトに関連付けられた C# オブジェクトを取得します。
        /// </summary>
        /// <typeparam name="T">取り出す値の型。</typeparam>
        /// <param name="rbObject">Ruby オブジェクト。</param>
        /// <returns>C# オブジェクト。</returns>
        internal T GetCachedObject<T>(VALUE rbObject)
        {
            int key = RUBY.GetID(rbObject);
            object obj;
            if (this.Cache.TryGetValue(key, out obj))
            {
                return (T)obj;
            }
            else
            {
                string message = string.Format("C# object was not found: {0}", RUBY.StringValuePtr(RUBY.Send(rbObject, "inspect")));
                RUBY.rb_raise(SERubyConsts.ForName("StarEngineError"), message);
                return default(T);
            }
        }

        internal bool ContainsCachedObject(VALUE rbObject)
        {
            return this.Cache.ContainsKey(RUBY.GetID(rbObject));
        }

        /// <summary>
        /// Ruby オブジェクトと C# オブジェクトとの関連を削除します。
        /// </summary>
        /// <param name="rbObject">Ruby オブジェクトの ID (C# の int)。</param>
        internal void RemoveCachedObject(int id)
        {
            this.Cache.Remove(id);
        }
    }
}
