using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.Xml;
using System.Xml.Serialization;
using tom;

namespace VPScriptEditor
{
    public partial class VPScriptEditor
    {
        private Pointer[] inputPtr, outputPtr;
        private PointerAnnotation[] annotations;
        private ITextDocument inDoc, outDoc;
        private int currPointerNb, nbUndefinedPtr, nextUntranslatedPtr, nextUnsurePtr;
        private string outputFileName;
        private FontFile vpFont;
        private Bitmap conersBmp;

        public VPScriptEditor()
        {
            InitializeComponent();

            StreamReader dfile = System.IO.File.OpenText(System.Windows.Forms.Application.StartupPath + @"\database\VP-database.xml");
            vpFont = new FontFile(dfile, Color.Black);
            dfile.Close();

            Stream imgStream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("VPScriptEditor.corners.png");
            if (imgStream == null) throw new Exception("Can't open embedded image");
            conersBmp = new Bitmap(imgStream);

            inDoc = getDocument(richEditInputPtr);
            outDoc = getDocument(richEditOutputPtr);

            updateTitle();
        }

        [System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
        private extern static IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, out IntPtr lParam);
        private const int WM_USER = 0x400, EM_GETOLEINTERFACE = WM_USER + 60;
        private ITextDocument getDocument(RichTextBox rtb)
        {
            IntPtr iRichEditOle = IntPtr.Zero;
            if (SendMessage(rtb.Handle, EM_GETOLEINTERFACE, 0, out iRichEditOle) == IntPtr.Zero)
                throw new Exception("Can't access IRichEditOle object from RichTextBox");

            return (ITextDocument)System.Runtime.InteropServices.Marshal.GetTypedObjectForIUnknown(iRichEditOle, typeof(tom.ITextDocument));
        }

        private Pointer[] parseXML(String content)
        {
            Match match = Regex.Match(content.Replace("\r\n", "\n"), "^<roomscripts>\\n*((?:.|\\n)*?)(?:\\n\\n)?</roomscripts>\\n*$");
            if (!match.Success) throw new Exception("Bad script format!");

            MatchCollection matches = Regex.Matches(match.Groups[1].Value, "<ptr n=\"(\\d+)\" room=\"(.+?)\"/>\\n(<nowindowdetected/>|<window (type=\"fixed\"|x=\"(.+?)\" y=\"(.+?)\" width=\"(.+?)\" height=\"(.+?)\")/>)\\n((?:.|\\n)*?)(?=(?:\\n\\n<ptr n=\"\\d+\".*/>|$))");
            if (matches.Count == 0) throw new Exception("Void script!");

            Pointer[] pointers = new Pointer[matches.Count];

            for (int i = 0; i < matches.Count; i++)
            {
                int nb = int.Parse(matches[i].Groups[1].Value);
                if (nb <= 0 || nb > matches.Count) throw new Exception("Pointer no " + nb + " out of range!");

                if (pointers[nb - 1] != null) throw new Exception("Duplicate pointer no " + nb);
                pointers[nb - 1] = new Pointer(matches[i].Groups[2].Value, matches[i].Groups[5].Value.Length > 0 ? WindowType.Normal : (matches[i].Groups[4].Value.Length > 0 ? WindowType.Fixed : WindowType.None), matches[i].Groups[5].Value, matches[i].Groups[6].Value, matches[i].Groups[7].Value, matches[i].Groups[8].Value, matches[i].Groups[9].Value);
            }

            return pointers;
        }

        private string getAnnotationFileName(string filename)
        {
            return Path.GetDirectoryName(filename) + "\\" + Path.GetFileNameWithoutExtension(filename) + ".annotation.xml";
        }

        private void OpenMenuItem_Click(System.Object sender, System.EventArgs e)
        {
            StreamReader sr = null;

            try
            {
                OpenFileDialog ofd = new OpenFileDialog();
                ofd.Title = "Select *original* script file";
                ofd.Filter = "Valkyrie Profile script files (*.xml)|*.xml|" + "All files|*.*";
                if (ofd.ShowDialog() != DialogResult.OK)
                    return;

                String origFileName = ofd.FileName, outputFileName;
                sr = new StreamReader(ofd.OpenFile());
                Pointer[] inputPtr = parseXML(sr.ReadToEnd());
                sr.Close();

                ofd.Title = "Select *translated* script file";

                while (true)
                    if (ofd.ShowDialog() != DialogResult.OK)
                        return;
                    else
                        if ((outputFileName = ofd.FileName) == origFileName)
                            MessageBox.Show("Select a different file than the original script!", "Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                        else
                            break;

                sr = new StreamReader(ofd.OpenFile());
                Pointer[] outputPtr = parseXML(sr.ReadToEnd());
                sr.Close();
                if (outputPtr.Length != inputPtr.Length) throw new Exception("The two scripts don't contain same number of pointers");

                String annotationFileName = getAnnotationFileName(outputFileName);
                PointerAnnotation[] annotations;
                if (System.IO.File.Exists(annotationFileName))
                {
                    XmlSerializer serializer = new XmlSerializer(typeof(PointerAnnotation[]));
                    sr = new StreamReader(annotationFileName);
                    annotations = (PointerAnnotation[])serializer.Deserialize(sr);
                }
                else
                {
                    MessageBox.Show("No annotation file found for " + Path.GetFileName(outputFileName) + "\nA new one will be created on next save", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                    annotations = new PointerAnnotation[inputPtr.Length];
                    for (int i = 0; i < inputPtr.Length; i++)
                        annotations[i] = new PointerAnnotation();
                }

                this.inputPtr = inputPtr;
                this.outputPtr = outputPtr;
                this.outputFileName = outputFileName;
                this.annotations = annotations;

                nbUndefinedPtr = 0;
                foreach (PointerAnnotation pa in annotations)
                    if (pa.state == PointerState.Undefined)
                        nbUndefinedPtr++;

                toolStripStatusLabel1.Text = Path.GetFileName(outputFileName) + " successfully loaded";
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            finally
            {
                if (sr != null) sr.Close();
            }

            initInterface();
        }

        private void saveCurrentPointer()
        {
            outputPtr[currPointerNb].setVars(inputPtr[currPointerNb].rooms, inputPtr[currPointerNb].type, txtbxOutputX.Text, txtbxOutputY.Text, txtbxOutputWidth.Text, txtbxOutputHeight.Text, richEditOutputPtr.Text.Replace("\r\n", "\n"));
            annotations[currPointerNb].state = chkDone.Checked ? PointerState.Done : (chkUnsure.Checked ? PointerState.Unsure : PointerState.Undefined);
        }

        private bool saveOutput(string fileName)
        {
            saveCurrentPointer();

            StreamWriter sw = null;
            try
            {
                StringBuilder str = new StringBuilder("<roomscripts>\n\n");

                for (int i = 0; i < outputPtr.Length; i++)
                {
                    Pointer p = outputPtr[i];

                    str.Append("<ptr n=\"" + (i + 1) + "\" room=\"" + p.rooms + "\"/>\n");
                    if (p.type == WindowType.None)
                        str.Append("<nowindowdetected/>");
                    else
                        if (p.type == WindowType.Fixed)
                            str.Append("<window type=\"fixed\"/>");
                        else
                            str.Append("<window x=\"" + p.x + "\" y=\"" + p.y + "\" width=\"" + p.width + "\" height=\"" + p.height + "\"/>");

                    str.Append("\n");
                    str.Append(p.content.Replace("\r\n", "\n"));
                    str.Append("\n\n");
                }

                str.Append("</roomscripts>\n");

                sw = new StreamWriter(fileName);
                sw.Write(str);
                sw.Close();

                XmlSerializer serializer = new XmlSerializer(typeof(PointerAnnotation[]));
                sw = new StreamWriter(getAnnotationFileName(fileName));
                serializer.Serialize(sw, annotations);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }
            finally
            {
                if (sw != null) sw.Close();
            }

            toolStripStatusLabel1.Text = "Last save on " + DateTime.Now.ToString();
            return true;
        }

        private void updateInputPtr()
        {
            Pointer p = inputPtr[currPointerNb];

            txtbxRoom.Text = p.rooms;
            txtbxInputX.Text = p.x;
            txtbxInputY.Text = p.y;
            txtbxInputWidth.Text = p.width;
            txtbxInputHeight.Text = p.height;
            richEditInputPtr.Text = p.content;
        }

        private void updateOutputPtr()
        {
            Pointer p = outputPtr[currPointerNb];
            txtbxOutputX.Text = p.x;
            txtbxOutputY.Text = p.y;
            txtbxOutputWidth.Text = p.width;
            txtbxOutputHeight.Text = p.height;
            richEditOutputPtr.Text = p.content;
            txtbxOutputX.Enabled = txtbxOutputY.Enabled = txtbxOutputWidth.Enabled = txtbxOutputHeight.Enabled = bttnResize.Enabled = p.type == WindowType.Normal;

            PointerState ps = annotations[currPointerNb].state;
            chkDone.CheckedChanged -= this.chkbx_CheckedChanged;
            chkDone.Checked = ps == PointerState.Done;
            chkDone.CheckedChanged += this.chkbx_CheckedChanged;
            chkUnsure.CheckedChanged -= this.chkbx_CheckedChanged;
            chkUnsure.Checked = ps == PointerState.Unsure;
            chkUnsure.CheckedChanged += this.chkbx_CheckedChanged;
        }

        private void updateCommentBttn()
        {
            bttnComment.Text = annotations[currPointerNb].comment == null ? "Add comment" : "View/Edit comment";
        }

        private void updateInterface()
        {
            nextUntranslatedPtr = searchPointer(PointerState.Undefined);
            nextUnsurePtr = searchPointer(PointerState.Unsure);

            bttnNext.Enabled = currPointerNb < inputPtr.Length - 1;
            bttnPrevious.Enabled = currPointerNb > 0;
            bttnNextUntranslated.Enabled = nextUntranslatedPtr != -1;
            bttnNextUnsure.Enabled = nextUnsurePtr != -1;

            txtbxPointerNumber.Text = Convert.ToString(currPointerNb + 1);

            updateInputPtr();
            updateOutputPtr();
            updateCommentBttn();
            pnlInput.Invalidate();
            pnlOutput.Invalidate();
        }

        private void updateTitle()
        {
            this.Text = AboutBox.AssemblyProduct + (outputFileName != null ? " - " + Path.GetFileName(outputFileName) : "");
        }

        private void updateStatusProgress()
        {
            lblStatusProgress.Text = (inputPtr.Length - nbUndefinedPtr) + "/" + inputPtr.Length;
        }

        private void initInterface()
        {
            currPointerNb = 0;
            txtbxPointerNumber.Enabled = bttnReset.Enabled = SaveMenuItem.Enabled = SaveAsMenuItem.Enabled = chkDone.Enabled = chkUnsure.Enabled = bttnComment.Enabled = richEditOutputPtr.Enabled = true;

            updateInterface();
            updateTitle();
            updateStatusProgress();

            ActiveControl = richEditOutputPtr;
        }

        private int searchPointer(PointerState ps)
        {
            for (int i = 1; i < annotations.Length; i++)
                if (annotations[(currPointerNb + i) % annotations.Length].state == ps)
                    return (currPointerNb + i) % annotations.Length;

            return -1;
        }

        private void gotoPointer(int nb)
        {
            saveCurrentPointer();
            currPointerNb = nb;
            updateInterface();
        }

        private void txtbxPointerNumber_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == (char)Keys.Return)
            {
                e.Handled = true;
                int nb;
                if (int.TryParse(txtbxPointerNumber.Text, out nb) && nb > 0 && nb <= inputPtr.Length)
                    gotoPointer(nb - 1);
                else
                {
                    MessageBox.Show("Pointer out of range. Please enter a number between 1 and " + inputPtr.Length + ".", "Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                    txtbxPointerNumber.Text = System.Convert.ToString(currPointerNb + 1);
                }
            }
        }

        private void bttnPrevious_Click(System.Object sender, System.EventArgs e)
        {
            gotoPointer(currPointerNb - 1);
        }

        private void bttnNext_Click(System.Object sender, System.EventArgs e)
        {
            gotoPointer(currPointerNb + 1);
        }
        
        private void bttnNextUntranslated_Click(object sender, EventArgs e)
        {
            gotoPointer(nextUntranslatedPtr);
        }

        private void bttnNextUnsure_Click(object sender, EventArgs e)
        {
            gotoPointer(nextUnsurePtr);
        }

        private void bttnReset_Click(System.Object sender, System.EventArgs e)
        {
            outputPtr[currPointerNb].copyFrom(inputPtr[currPointerNb]);
            chkDone.Checked = chkUnsure.Checked = false;
            updateOutputPtr();
        }

        private void ExitMenuItem_Click(System.Object sender, System.EventArgs e)
        {
            Application.Exit();
        }

        private void SaveMenuItem_Click(System.Object sender, System.EventArgs e)
        {
            saveOutput(outputFileName);
        }

        private void SaveAsMenuItem_Click(object sender, EventArgs e)
        {
            SaveFileDialog sfd = new SaveFileDialog();
            sfd.Title = "Save as";
            sfd.Filter = "Valkyrie Profile script files (*.xml)|*.xml|" + "All files|*.*";

            if (sfd.ShowDialog() != DialogResult.OK)
                return;

            if (saveOutput(sfd.FileName))
            {
                outputFileName = sfd.FileName;
                updateTitle();
            }
        }

        private void colorizeRichEdit(ITextDocument itd)
        {
            itd.Freeze();
            itd.Undo((int)tomConstants.tomSuspend);

            ITextRange r = itd.Range(0, 0);
            r.MoveEnd((int)tomConstants.tomCharacter, r.StoryLength);
            r.Font.ForeColor = 0;

            MatchCollection matches = Regex.Matches(r.Text, "<(.*?)/>");
            for (int i = 0; i < matches.Count; i++)
            {
                r.Start = matches[i].Groups[0].Index;
                r.End = r.Start + matches[i].Groups[0].Length;
                r.Font.ForeColor = 0xFF;
            }

            itd.Undo((int)tomConstants.tomResume);
            itd.Unfreeze();
        }

        private void parseScriptLines(string[] input, int a, int b)
        {
            for (int i = a; i < b; i++)
                input[i] = Regex.Replace(Regex.Replace(input[i], "<st rep=\"(\\d+)\"/>(.+?)<rrep/>", new MatchEvaluator(delegate(Match m) { return new String(m.Groups[2].Value[0], int.Parse(m.Groups[1].Value)); })), "<.+?/>", "");
        }

        private static bool isNewTag(String s) { return s == "<new/>"; }
        
        private Bitmap drawPointer(RichTextBox rtb, string xStr, string yStr, string wStr, string hStr)
        {
            String[] text = Regex.Split(rtb.Text, "\n");
            int currLineNb = Regex.Matches(rtb.Text.Substring(0, rtb.SelectionStart), "\\n").Count;

            int begLineNb = Array.FindLastIndex(text, currLineNb, isNewTag) + 1, endLineNb = Array.FindIndex(text, currLineNb + 1, isNewTag);
            if (endLineNb < 0) endLineNb = text.Length;
            parseScriptLines(text, begLineNb, endLineNb);

            Bitmap bmp = new Bitmap(320, 240, PixelFormat.Format32bppArgb);
            Graphics gBmp = Graphics.FromImage(bmp);
            gBmp.CompositingMode = CompositingMode.SourceOver;

            gBmp.FillRectangle(Brushes.White, 0, 0, bmp.Width, bmp.Height);

            int x_base, y_curr, h, w;

            if (!int.TryParse(xStr, out x_base) || !int.TryParse(yStr, out y_curr))
                x_base = y_curr = 10;

            if (int.TryParse(wStr, out w) && int.TryParse(hStr, out h))
            {
                SolidBrush innerBrush = new SolidBrush(Color.FromArgb(248, 216, 96)), outerBrush = new SolidBrush(Color.FromArgb(88, 64, 8));
                gBmp.FillRectangle(outerBrush, x_base - 8, y_curr, 1, h);
                gBmp.FillRectangle(innerBrush, x_base - 7, y_curr, 2, h);
                gBmp.FillRectangle(outerBrush, x_base - 2, y_curr - 7, w + 5, 1);
                gBmp.FillRectangle(innerBrush, x_base - 2, y_curr - 6, w + 5, 2);
                gBmp.FillRectangle(outerBrush, x_base + w + 9, y_curr, 1, h);
                gBmp.FillRectangle(innerBrush, x_base + w + 7, y_curr, 2, h);
                gBmp.FillRectangle(outerBrush, x_base - 2, y_curr + h + 6, w + 5, 1);
                gBmp.FillRectangle(innerBrush, x_base - 2, y_curr + h + 4, w + 5, 2);

                gBmp.DrawImage(conersBmp, x_base - 8, y_curr - 7, new Rectangle(0, 0, 7, 7), GraphicsUnit.Pixel);
                gBmp.DrawImage(conersBmp, x_base - 8, y_curr + h, new Rectangle(0, 7, 7, 7), GraphicsUnit.Pixel);
                gBmp.DrawImage(conersBmp, x_base + w + 3, y_curr - 7, new Rectangle(7, 0, 7, 7), GraphicsUnit.Pixel);
                gBmp.DrawImage(conersBmp, x_base + w + 3, y_curr + h, new Rectangle(7, 7, 7, 7), GraphicsUnit.Pixel);
            }

            int x_curr = x_base;
            for (int j = begLineNb; j < endLineNb; j++)
            {
                foreach (char ch in text[j])
                    if (vpFont.containsSymbol(ch))
                    {
                        gBmp.DrawImage(vpFont.getBitmap(ch), x_curr, y_curr);
                        x_curr += vpFont.getSymbolWidth(ch);
                    }

                x_curr = x_base;
                y_curr += 14;
            }

            return bmp;
        }

        private void pnlInput_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.DrawImage(drawPointer(richEditInputPtr, txtbxInputX.Text, txtbxInputY.Text, txtbxInputWidth.Text, txtbxInputHeight.Text), 0, 0);
        }

        private void pnlOutput_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.DrawImage(drawPointer(richEditOutputPtr, txtbxOutputX.Text, txtbxOutputY.Text, txtbxOutputWidth.Text, txtbxOutputHeight.Text), 0, 0);
        }

        private void richEdit_SelectionChanged(object sender, EventArgs e)
        {
            ((RichTextBox)sender == richEditInputPtr ? pnlInput : pnlOutput).Invalidate();
        }

        private void richEditInputPtr_TextChanged(object sender, EventArgs e)
        {
            colorizeRichEdit(inDoc);
        }

        private void richEditOutputPtr_SelectionChanged(object sender, EventArgs e)
        {
            pnlOutput.Invalidate();
        }

        private void richEditOutputPtr_TextChanged(object sender, EventArgs e)
        {
            colorizeRichEdit(outDoc);
            // simple trick to circumvent a stupid vertical scroll bug
            richEditOutputPtr.SelectionStart = richEditOutputPtr.SelectionStart;
        }

        private void richEditOutputPtr_KeyDown(object sender, KeyEventArgs e)
        {
            if ((e.Shift && e.KeyCode == Keys.Insert) || (e.Control && e.KeyCode == Keys.V))
            {
                richEditOutputPtr.Paste(DataFormats.GetFormat("Text"));
                e.Handled = true;
            }
            else
                if ((e.Modifiers == Keys.None || e.Modifiers == Keys.Control) && e.KeyCode == Keys.Delete)
                    pnlOutput.Invalidate();
        }

        private void richEditOutputPtr_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar >= ' ' && e.KeyChar != (char)Keys.F16 && !vpFont.containsSymbol(e.KeyChar))
            {
                e.Handled = true;
                System.Media.SystemSounds.Beep.Play();
            }
        }

        private void bttnResize_Click(System.Object sender, System.EventArgs e)
        {
            String[] text = Regex.Split(richEditOutputPtr.Text, "\n");
            int w = 0, h = 0, curr_h = 0;

            for (int j = 0; j < text.Length; j++)
                if (isNewTag(text[j]))
                {
                    h = Math.Max(curr_h, h);
                    curr_h = 0;
                }
                else
                {
                    int curr_w = 0;
                    parseScriptLines(text, j, j + 1);

                    foreach (char ch in text[j])
                        if (vpFont.containsSymbol(ch))
                            curr_w += vpFont.getSymbolWidth(ch);

                    w = Math.Max(curr_w, w);
                    curr_h += 14;
                }

            h = Math.Max(curr_h, h);

            txtbxOutputWidth.Text = System.Convert.ToString(w);
            txtbxOutputHeight.Text = System.Convert.ToString(h);

            pnlOutput.Invalidate();
        }

        private void chkbx_CheckedChanged(object sender, EventArgs e)
        {
            CheckBox chk = (CheckBox)sender, chk2 = chk == chkDone ? chkUnsure : chkDone;
            if (chk.Checked && chk2.Checked)
            {
                nbUndefinedPtr++;
                chk2.Checked = false;
            }
            else
            {
                nbUndefinedPtr += (!chkDone.Checked && !chkUnsure.Checked ? 1 : -1);
                updateStatusProgress();
            }
        }

        private void bttnComment_Click(object sender, EventArgs e)
        {
            CommentForm cf = new CommentForm(currPointerNb, annotations[currPointerNb].comment);
            cf.ShowDialog();
            annotations[currPointerNb].comment = cf.Comment;
            updateCommentBttn();
        }

        private void txtbxOutput_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == (char)Keys.Return)
            {
                e.Handled = true;
                pnlOutput.Invalidate();
            }
        }

        private void wordWarpToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
        {
            richEditInputPtr.WordWrap = richEditOutputPtr.WordWrap = ((ToolStripMenuItem)sender).Checked;
            inDoc = getDocument(richEditInputPtr);
            outDoc = getDocument(richEditOutputPtr);
        }

        private void aboutToolStripMenuItem1_Click(object sender, EventArgs e)
        {
            new AboutBox().ShowDialog();
        }
    }
}