using System;
using System.Collections.Generic;
using ChaKi.Entity.Corpora;
using ChaKi.Entity.Corpora.Annotations;
using NHibernate;
using ChaKi.Service.Common;
using System.Text;


namespace ChaKi.Service.DependencyEdit
{
    internal abstract class Operation
    {
        public abstract void Execute(DepEditContext ctx);

        public abstract void UnExecute(DepEditContext ctx);

        public virtual string ToIronRubyStatement(DepEditContext ctx) { return string.Empty; }
        public virtual string ToIronPythonStatement(DepEditContext ctx) { return string.Empty; }


        // 以下、複数のOperationで共通のメソッド類

        public static string APPOSITION_TAG_NAME = "Apposition";
        public static string PARALLEL_TAG_NAME = "Parallel";
        public static string NEST_TAG_NAME = "Nest";

        public static string LastTagUsed = APPOSITION_TAG_NAME;

        protected void ExecuteSplitBunsetsuOperation(int docid, int bpos, int epos, int spos, DepEditContext ctx)
        {
            // Split範囲にあるSegmentを取得
            Segment left = FindSegmentBetween(docid, bpos, epos, ctx);
            if (left == null)
            {
                throw new InvalidOperationException("Splitting: Missing segment");
            }
            // leftセグメントの終端
            left.EndChar = spos;
            // 新しいセグメント(right)
            Segment right = new Segment();
            right.StartChar = spos;
            right.EndChar = epos;
            right.Tag = left.Tag;
            right.Doc = left.Doc;
            right.Proj = ctx.Proj;
            right.User = ctx.User;
            right.Version = left.Version;
            right.Sentence = left.Sentence;
            // rightをDBに保存
            ctx.Save(right);
            // leftから出ているLinkをrightから出るように変更
            Link oldlink = FindLinkFrom(left, ctx);
            if (oldlink == null)
            {
                throw new InvalidOperationException("Splitting: Missing link");
            }
            oldlink.From = right;
            // left（既存）からright（新規）SegmentへLinkを張る
            Link newlink = new Link();
            newlink.From = left;
            newlink.To = right;
            newlink.Tag = oldlink.Tag;
            newlink.FromSentence = left.Sentence;
            newlink.ToSentence = right.Sentence;
            newlink.Proj = oldlink.Proj;
            newlink.User = ctx.User;
            newlink.Version = oldlink.Version;
            ctx.Save(newlink);
        }

        protected void ExecuteMergeBunsetsuOperation(int docid, int bpos, int epos, DepEditContext ctx)
        {
            Segment bseg = FindSegmentStartingAt(docid, bpos, ctx);
            if (bseg == null)
            {
                throw new InvalidOperationException("Merging: Missing first segment");
            }
            Segment eseg = FindSegmentEndingAt(docid, epos, ctx);
            if (eseg == null)
            {
                throw new InvalidOperationException("Merging: Missing second segment");
            }
            if (bseg == eseg)
            {
                throw new InvalidOperationException("Merging: Cannot merge single segment");
            }

            // bsegから出るLink
            Link blink = FindLinkFrom(bseg, ctx);
            if (blink == null)
            {
                throw new InvalidOperationException("Merging: Missing link");
            }
            // esegから出るLink
            Link elink = FindLinkFrom(eseg, ctx);
            if (elink == null)
            {
                throw new InvalidOperationException("Merging: Missing link");
            }
            // esegに入るLink
            IList<Link> links = FindLinksTo(eseg, ctx);

            // leftセグメントを右に拡張することでマージする
            bseg.EndChar = epos;
            // bsegから出るリンクの係り先をelinkの係り先に変更
            blink.To = elink.To;
            // esegに入っていたリンクの係り先をすべてbsegに変更
            foreach (object o in links)
            {
                Link lk = o as Link;
                if (lk.From != bseg)
                {
                    lk.To = bseg;
                }
            }
            // esegから出るリンクをDBから切り離す
            ctx.Delete(elink);
            // 削除された文節をDBから切り離し、Transientなオブジェクトにする。
            ctx.Delete(eseg);
        }

        protected void ExecuteSplitWordOperation(int docid, int bpos, int epos, int spos, Lexeme lex1, Lexeme lex2, ref Lexeme orglex, DepEditContext ctx)
        {
            // Split範囲にあるWordを取得
            Word left = FindWordBetween(docid, bpos, epos, ctx);
            if (left == null)
            {
                throw new InvalidOperationException("Splitting: Missing word");
            }
            // 元のLexeme IDを保存
            orglex = left.Lex;

            // left Wordの終端
            left.EndChar = spos;
            // 元の表層形
            left.Lex = lex1;
            // 新しい Word(right)
            Word right = new Word();
            right.StartChar = spos;
            right.EndChar = epos;
            right.Bunsetsu = left.Bunsetsu;
            right.Lex = lex2;
            right.Sen = left.Sen;

            ctx.Save(right);

            // Sentence-Wordの関係を調整.
            // Word.Posを付け直す.
            int pos = 0;
            foreach (Word w in left.Sen.Words)
            {
                w.Pos = pos++;
                if (w == left)
                {
                    right.Pos = pos++;
                }
            }
            ctx.Flush();
            // Word.Posを元に、メモリ内のSentence.Wordsリストを同期する
            ctx.Session.Refresh(left.Sen);

            ctx.AddRelevantLexeme(orglex);
            ctx.AddRelevantLexeme(lex1);
            ctx.AddRelevantLexeme(lex2);
        }

        protected void ExecuteMergeWordOperation(int docid, int bpos, int epos, Lexeme lex, ref Lexeme lex1org, ref Lexeme lex2org, DepEditContext ctx)
        {
            Word bword = FindWordStartingAt(docid, bpos, ctx);
            if (bword == null)
            {
                throw new InvalidOperationException("Merging: Missing first word");
            }
            Word eword = FindWordEndingAt(docid, epos, ctx);
            if (eword == null)
            {
                throw new InvalidOperationException("Merging: Missing second word");
            }
            if (bword == eword)
            {
                throw new InvalidOperationException("Merging: Cannot merge single word");
            }

            lex1org = bword.Lex;
            lex2org = eword.Lex;

            // left Wordを右に拡張することでマージする
            bword.EndChar = epos;
            bword.Lex = lex;
            // 削除されたWordをDBから切り離し、Transientなオブジェクトにする。
            ctx.Delete(eword);
            ctx.Flush();

            // Sentence-Wordの関係を調整.
            // Word.Posを付け直す.
            int pos = 0;
            foreach (Word w in bword.Sen.Words)
            {
                if (w == null || w == eword)
                {
                    continue;
                }
                w.Pos = pos++;
            }
            ctx.Flush();
            // Word.Posを元に、メモリ内のSentence.Wordsリストを同期する
            ctx.Session.Refresh(bword.Sen);

            ctx.AddRelevantLexeme(lex);
            ctx.AddRelevantLexeme(lex1org);
            ctx.AddRelevantLexeme(lex2org);
        }

        protected Segment FindSegmentBetween(int docid, int bpos, int epos, DepEditContext ctx)
        {
            IQuery query = ctx.Session.CreateQuery(         //TODO: Seg.TagはTagSetからBunsetsuのIDを得なければならない
                string.Format("from Segment seg where seg.Doc.ID={0} and seg.StartChar={1} and seg.EndChar={2} and seg.Tag.Name='Bunsetsu'", docid, bpos, epos));
            return query.UniqueResult<Segment>();
        }

        protected Segment FindSegmentStartingAt(int docid, int pos, DepEditContext ctx)
        {
            // 文の開始終了位置に、長さ0のDummy Segmentが存在する。Segmentを同定するクエリでこれをヒットしないように注意。
            IQuery query = ctx.Session.CreateQuery(         //TODO: Seg.TagはTagSetからBunsetsuのIDを得なければならない
                string.Format("from Segment seg where seg.Doc.ID={0} and seg.StartChar={1} and seg.EndChar>{1} and seg.Tag.Name='Bunsetsu'", docid, pos));
            return query.UniqueResult<Segment>();
        }

        protected Segment FindSegmentEndingAt(int docid, int pos, DepEditContext ctx)
        {
            IQuery query = ctx.Session.CreateQuery(         //TODO: Seg.TagはTagSetからBunsetsuのIDを得なければならない
                string.Format("from Segment seg where seg.Doc.ID={0} and seg.EndChar={1} and seg.StartChar<{1} and seg.Tag.Name='Bunsetsu'", docid, pos));
            return query.UniqueResult<Segment>();
        }

        /// <summary>
        /// 重複関連であるWord-Segmentを割り当て直す.
        /// </summary>
        protected void UpdateWordToSegmentRelations(DepEditContext ctx)
        {
            IQuery query = ctx.Session.CreateQuery(
                string.Format("from Segment seg where seg.Sentence.ID={0} and seg.Tag.Name='Bunsetsu'", ctx.Sen.ID));
            IList<Segment> segs = query.List<Segment>();
            query = ctx.Session.CreateQuery(
                string.Format("from Word w where w.Sen.ID={0} order by w.ID ", ctx.Sen.ID));
            IList<Word> words = query.List<Word>();

            foreach (Word w in words)
            {
                int startChar = w.StartChar;
                int endChar = w.EndChar;
                foreach (Segment seg in segs)
                {
                    if (startChar >= seg.StartChar && endChar <= seg.EndChar)
                    {
                        w.Bunsetsu = seg;
                        break;
                    }
                }
            }
        }

        public static Link FindLinkFrom(Segment seg, DepEditContext ctx)
        {
            IQuery query = ctx.Session.CreateQuery(string.Format("select lk from Link lk, Segment seg where seg.ID={0} and lk.From=seg", seg.ID));
            return query.UniqueResult<Link>();
        }

        public static IList<Link> FindLinksTo(Segment seg, DepEditContext ctx)
        {
            IQuery query = ctx.Session.CreateQuery(string.Format("select lk from Link lk, Segment seg where seg.ID={0} and lk.To=seg", seg.ID));
            return query.List<Link>();
        }

        public static Tag FindBunsetsuTag(DepEditContext ctx)
        {
            IQuery query = ctx.Session.CreateQuery("from Tag tag where tag.Type='Segment' and tag.Name='Bunsetsu'");
            return query.UniqueResult<Tag>();
        }

        public static Tag FindLinkTag(string linkTagName, DepEditContext ctx)
        {
            IQuery query = ctx.Session.CreateQuery(string.Format("from Tag tag where tag.Type='Link' and tag.Name='{0}'", linkTagName));
            return query.UniqueResult<Tag>();
        }

        public static Word FindWordBetween(int docid, int bpos, int epos, DepEditContext ctx)
        {
            IQuery query = ctx.Session.CreateQuery(
                string.Format("from Word w where w.Sen.ParentDoc.ID={0} and w.StartChar={1} and w.EndChar={2}", docid, bpos, epos));
            IList<Word> result = query.List<Word>();
            return query.UniqueResult<Word>();
        }

        public static Word FindWordStartingAt(int docid, int pos, DepEditContext ctx)
        {
            IQuery query = ctx.Session.CreateQuery(
                string.Format("from Word w where w.Sen.ParentDoc.ID={0} and w.StartChar={1} and w.EndChar>{1}", docid, pos));
            return query.UniqueResult<Word>();
        }

        public static Word FindWordEndingAt(int docid, int pos, DepEditContext ctx)
        {
            IQuery query = ctx.Session.CreateQuery(
                string.Format("from Word w where w.Sen.ParentDoc.ID={0} and w.EndChar={1} and w.StartChar<{1}", docid, pos));
            return query.UniqueResult<Word>();
        }

        public static Lexeme FindLexeme(int id, DepEditContext ctx)
        {
            IQuery query = ctx.Session.CreateQuery(string.Format("from Lexeme l where l.ID={0}", id));
            return query.UniqueResult<Lexeme>();
        }

        /// <summary>
        /// propsで与えられた内容と完全に（空の("")Propertyを含め）一致するLexemeが既にLexiconに存在すれば、一致するLexemeを返す.
        /// 該当するLexemeが複数ある場合は最初に見つかったものを返す.
        /// なければnullを返す.
        /// </summary>
        public static Lexeme FindLexeme(string[] props, DepEditContext ctx)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("from Lexeme l where");
            sb.AppendFormat(" l.Surface='{0}'", props[(int)LP.Surface]);
            sb.AppendFormat(" and l.Reading='{0}'", props[(int)LP.Reading]);
            sb.AppendFormat(" and l.LemmaForm='{0}'", props[(int)LP.LemmaForm]);
            sb.AppendFormat(" and l.Pronunciation='{0}'", props[(int)LP.Pronunciation]);
            sb.AppendFormat(" and l.BaseLexeme.Surface='{0}'", props[(int)LP.BaseLexeme]);
            sb.AppendFormat(" and l.Lemma='{0}'", props[(int)LP.Lemma]);
            sb.AppendFormat(" and l.PartOfSpeech.Name='{0}'", props[(int)LP.PartOfSpeech]);
            sb.AppendFormat(" and l.CType.Name='{0}'", props[(int)LP.CType]);
            sb.AppendFormat(" and l.CForm.Name='{0}'", props[(int)LP.CForm]);

            IQuery q = ctx.Session.CreateQuery(sb.ToString());
            IList<Lexeme> result = q.List<Lexeme>();
            if (result.Count > 0)
            {
                return result[0];
            }
            return null;
        }

        /// <summary>
        /// propsで与えられた内容を元に既存のLexemeを更新または新たに生成してDBに登録する.
        /// Base, POS, CForm, CTypeも必要に応じて登録する.
        /// </summary>
        /// <param name="lex">既存ならID >= 0, 新語ならID &lt; 0</param>
        /// <param name="props">LPの順に並べられたProperty文字列の配列</param>
        public static void CreateOrUpdateLexeme(ref Lexeme lex, string[] props, string customprop, DepEditContext ctx)
        {
            IQuery q;

            if (lex == null)
            {
                lex = new Lexeme();
            }

            // POS
            q = ctx.Session.CreateQuery(string.Format("from PartOfSpeech p where p.Name='{0}'", props[(int)LP.PartOfSpeech]));
            PartOfSpeech pos = q.UniqueResult<PartOfSpeech>();
            if (pos == null)
            {
                pos = new PartOfSpeech(props[(int)LP.PartOfSpeech]);
                ctx.Save(pos);
            }
            lex.PartOfSpeech = pos;
            // CType
            q = ctx.Session.CreateQuery(string.Format("from CType t where t.Name='{0}'", props[(int)LP.CType]));
            CType ctype = q.UniqueResult<CType>();
            if (ctype == null)
            {
                ctype = new CType(props[(int)LP.CType]);
                ctx.Save(ctype);
            }
            lex.CType = ctype;
            // CForm
            q = ctx.Session.CreateQuery(string.Format("from CForm f where f.Name='{0}'", props[(int)LP.CForm]));
            CForm cform = q.UniqueResult<CForm>();
            if (cform == null)
            {
                cform = new CForm(props[(int)LP.CForm]);
                ctx.Save(cform);
            }
            lex.CForm = cform;

            // その他のProperty
            lex.Surface = props[(int)LP.Surface];
            lex.Reading = props[(int)LP.Reading];
            lex.Pronunciation = props[(int)LP.Pronunciation];
            lex.Lemma = props[(int)LP.Lemma];
            lex.LemmaForm = props[(int)LP.LemmaForm];
            lex.CustomProperty = customprop;

            // Base
            string basestr = props[(int)LP.BaseLexeme];
            Lexeme baselex = lex; // default
            if (basestr.Length > 0 && basestr != props[(int)LP.Surface])
            {
                // 基本形に一致するLexemeが既に存在するか?
                props[(int)LP.Surface] = basestr;
                baselex = FindLexeme(props, ctx);
                if (baselex == null)
                {
                    baselex = new Lexeme(lex);
                    baselex.Surface = basestr;
                    baselex.BaseLexeme = baselex;
                    ctx.Save(baselex);
                    ctx.AddRelevantLexeme(baselex);
                }
            }
            lex.BaseLexeme = baselex;


            // LexemeをSave
            if (lex.ID < 0)
            {
                ctx.Save(lex);
                lex.Dictionary = null;
            }
            else
            {
                ctx.SaveOrUpdate(lex);
            }
            ctx.AddRelevantLexeme(lex);
        }
    }
}
