/*
 *  Copyright (C) 2010-2021 JPEXS, All rights reserved.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.
 */
package com.jpexs.decompiler.flash.tags.base;

import com.jpexs.decompiler.flash.AppResources;
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.SWFInputStream;
import com.jpexs.decompiler.flash.SWFOutputStream;
import com.jpexs.decompiler.flash.configuration.Configuration;
import com.jpexs.decompiler.flash.exporters.commonshape.ExportRectangle;
import com.jpexs.decompiler.flash.exporters.commonshape.Matrix;
import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter;
import com.jpexs.decompiler.flash.helpers.HighlightedText;
import com.jpexs.decompiler.flash.helpers.HighlightedTextWriter;
import com.jpexs.decompiler.flash.helpers.hilight.HighlightSpecialType;
import com.jpexs.decompiler.flash.tags.text.ParsedSymbol;
import com.jpexs.decompiler.flash.tags.text.TextAlign;
import com.jpexs.decompiler.flash.tags.text.TextLexer;
import com.jpexs.decompiler.flash.tags.text.TextParseException;
import com.jpexs.decompiler.flash.types.BasicType;
import com.jpexs.decompiler.flash.types.ColorTransform;
import com.jpexs.decompiler.flash.types.GLYPHENTRY;
import com.jpexs.decompiler.flash.types.MATRIX;
import com.jpexs.decompiler.flash.types.RECT;
import com.jpexs.decompiler.flash.types.RGB;
import com.jpexs.decompiler.flash.types.RGBA;
import com.jpexs.decompiler.flash.types.TEXTRECORD;
import com.jpexs.decompiler.flash.types.annotations.SWFType;
import com.jpexs.helpers.ByteArrayRange;
import com.jpexs.helpers.Helper;
import com.jpexs.helpers.SerializableImage;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 *
 * @author JPEXS
 */
public abstract class StaticTextTag extends TextTag {

    @SWFType(BasicType.UI16)
    public int characterID;

    protected int glyphBits;

    protected int advanceBits;

    public RECT textBounds;

    public MATRIX textMatrix;

    public List<TEXTRECORD> textRecords;

    public abstract int getTextNum();

    public StaticTextTag(SWF swf, int id, String name, ByteArrayRange data) {
        super(swf, id, name, data);
    }

    @Override
    public final void readData(SWFInputStream sis, ByteArrayRange data, int level, boolean parallel, boolean skipUnusualTags, boolean lazy) throws IOException {
        characterID = sis.readUI16("characterID");
        textBounds = sis.readRECT("textBounds");
        textMatrix = sis.readMatrix("textMatrix");
        glyphBits = sis.readUI8("glyphBits");
        advanceBits = sis.readUI8("advanceBits");
        textRecords = new ArrayList<>();
        TEXTRECORD tr;
        while ((tr = sis.readTEXTRECORD(getTextNum(), glyphBits, advanceBits, "record")) != null) {
            textRecords.add(tr);
        }
    }

    /**
     * Gets data bytes
     *
     * @param sos SWF output stream
     * @throws java.io.IOException
     */
    @Override
    public void getData(SWFOutputStream sos) throws IOException {
        sos.writeUI16(characterID);
        sos.writeRECT(textBounds);
        sos.writeMatrix(textMatrix);

        int glyphBits = 0;
        int advanceBits = 0;
        for (TEXTRECORD tr : textRecords) {
            for (GLYPHENTRY ge : tr.glyphEntries) {
                glyphBits = SWFOutputStream.enlargeBitCountU(glyphBits, ge.glyphIndex);
                advanceBits = SWFOutputStream.enlargeBitCountS(advanceBits, ge.glyphAdvance);
            }
        }

        if (Configuration._debugCopy.get()) {
            glyphBits = Math.max(glyphBits, this.glyphBits);
            advanceBits = Math.max(advanceBits, this.advanceBits);
        }

        sos.writeUI8(glyphBits);
        sos.writeUI8(advanceBits);
        for (TEXTRECORD tr : textRecords) {
            sos.writeTEXTRECORD(tr, getTextNum(), glyphBits, advanceBits);
        }
        sos.writeUI8(0);
    }

    @Override
    public RECT getBounds() {
        return textBounds;
    }

    @Override
    public MATRIX getTextMatrix() {
        return textMatrix;
    }

    @Override
    public void setBounds(RECT r) {
        textBounds = r;
    }

    @Override
    public List<String> getTexts() {
        FontTag fnt = null;
        List<String> ret = new ArrayList<>();
        for (TEXTRECORD rec : textRecords) {
            if (rec.styleFlagsHasFont) {
                FontTag fnt2 = swf.getFont(rec.fontId);
                if (fnt2 != null) {
                    fnt = fnt2;
                }
            }
            if (rec.styleFlagsHasXOffset || rec.styleFlagsHasYOffset) {
                /*if (!ret.isEmpty()) {
                 ret += "\r\n";
                 }*/
            }

            if (fnt == null) {
                ret.add(AppResources.translate("fontNotFound").replace("%fontId%", Integer.toString(rec.fontId)));
            } else {
                ret.add(rec.getText(fnt));
            }
        }
        return ret;
    }

    @Override
    public List<Integer> getFontIds() {
        List<Integer> ret = new ArrayList<>();
        for (TEXTRECORD rec : textRecords) {
            if (rec.styleFlagsHasFont) {
                ret.add(rec.fontId);
            }
        }
        return ret;
    }

    @Override
    public void updateTextBounds() {
        updateTextBounds(textBounds);
    }

    @Override
    public boolean alignText(TextAlign textAlign) {
        alignText(swf, textRecords, textAlign);
        setModified(true);
        return true;
    }

    @Override
    public boolean translateText(int diff) {
        textMatrix.translateX += diff;
        updateTextBounds();
        setModified(true);
        return true;
    }

    @Override
    public RECT getRect(Set<BoundedTag> added) {
        return textBounds;
    }

    @Override
    public ExportRectangle calculateTextBounds() {
        return calculateTextBounds(swf, textRecords, getTextMatrix());
    }

    @Override
    public int getNumFrames() {
        return 1;
    }

    @Override
    public boolean isSingleFrame() {
        return true;
    }

    @Override
    public int getCharacterId() {
        return characterID;
    }

    @Override
    public void setCharacterId(int characterId) {
        this.characterID = characterId;
    }

    @Override
    public HighlightedText getFormattedText(boolean ignoreLetterSpacing) {
        FontTag fnt = null;
        HighlightedTextWriter writer = new HighlightedTextWriter(Configuration.getCodeFormatting(), true);
        writer.append("[").newLine();
        writer.append("xmin ").append(textBounds.Xmin).newLine();
        writer.append("ymin ").append(textBounds.Ymin).newLine();
        writer.append("xmax ").append(textBounds.Xmax).newLine();
        writer.append("ymax ").append(textBounds.Ymax).newLine();
        if (textMatrix.translateX != 0) {
            writer.append("translatex ").append(textMatrix.translateX).newLine();
        }
        if (textMatrix.translateY != 0) {
            writer.append("translatey ").append(textMatrix.translateY).newLine();
        }
        if (textMatrix.hasScale) {
            writer.append("scalex ").append(textMatrix.scaleX).newLine();
            writer.append("scaley ").append(textMatrix.scaleY).newLine();
        }
        if (textMatrix.hasRotate) {
            writer.append("rotateskew0 ").append(textMatrix.rotateSkew0).newLine();
            writer.append("rotateskew1 ").append(textMatrix.rotateSkew1).newLine();
        }
        writer.append("]");
        int textHeight = 12;
        for (TEXTRECORD rec : textRecords) {
            if (rec.styleFlagsHasFont || rec.styleFlagsHasColor || rec.styleFlagsHasXOffset || rec.styleFlagsHasYOffset) {
                writer.append("[").newLine();
                if (rec.styleFlagsHasFont) {
                    FontTag fnt2 = swf.getFont(rec.fontId);
                    if (fnt2 != null) {
                        fnt = fnt2;
                    }
                    writer.append("font ").append(rec.fontId).newLine();
                    writer.append("height ").append(rec.textHeight).newLine();
                    textHeight = rec.textHeight;
                }
                if (fnt != null && !ignoreLetterSpacing) {
                    int letterSpacing = detectLetterSpacing(rec, fnt, textHeight);
                    if (letterSpacing != 0) {
                        writer.append("letterspacing ").append(letterSpacing).newLine();
                    }
                }
                if (rec.styleFlagsHasColor) {
                    if (getTextNum() == 1) {
                        writer.append("color ").append(rec.textColor.toHexRGB()).newLine();
                    } else {
                        writer.append("color ").append(rec.textColorA.toHexARGB()).newLine();
                    }
                }
                if (rec.styleFlagsHasXOffset) {
                    writer.append("x ").append(rec.xOffset).newLine();
                }
                if (rec.styleFlagsHasYOffset) {
                    writer.append("y ").append(rec.yOffset).newLine();
                }
                writer.append("]");
            }

            if (fnt == null) {
                writer.append(AppResources.translate("fontNotFound").replace("%fontId%", Integer.toString(rec.fontId)));
            } else {
                writer.hilightSpecial(Helper.escapeActionScriptString(rec.getText(fnt)).replace("[", "\\[").replace("]", "\\]"), HighlightSpecialType.TEXT);
            }
        }
        return new HighlightedText(writer);
    }

    @Override
    public boolean setFormattedText(MissingCharacterHandler missingCharHandler, String formattedText, String[] texts) throws TextParseException {
        try {
            TextLexer lexer = new TextLexer(new StringReader(formattedText));
            ParsedSymbol s = null;
            List<TEXTRECORD> textRecords = new ArrayList<>();
            RGB color = null;
            RGBA colorA = null;
            int fontId = -1;
            int textHeight = -1;
            int letterSpacing = 0;
            FontTag font = null;
            Integer x = null;
            Integer y = null;
            int currentX = 0;
            int currentY = 0;
            int maxX = Integer.MIN_VALUE;
            int minX = Integer.MAX_VALUE;
            MATRIX textMatrix = new MATRIX();
            textMatrix.hasRotate = false;
            textMatrix.hasScale = false;
            RECT textBounds = new RECT();
            int textIdx = 0;
            while ((s = lexer.yylex()) != null) {
                switch (s.type) {
                    case PARAMETER:
                        String paramName = (String) s.values[0];
                        String paramValue = (String) s.values[1];
                        switch (paramName) {
                            case "color":
                                if (getTextNum() == 1) {
                                    Matcher m = Pattern.compile("#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])").matcher(paramValue);
                                    if (m.matches()) {
                                        color = new RGB(Integer.parseInt(m.group(1), 16), Integer.parseInt(m.group(2), 16), Integer.parseInt(m.group(3), 16));
                                    } else {
                                        throw new TextParseException("Invalid color. Valid format is #rrggbb. Found: " + paramValue, lexer.yyline());
                                    }
                                } else {
                                    Matcher m = Pattern.compile("#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])").matcher(paramValue);
                                    if (m.matches()) {
                                        colorA = new RGBA(Integer.parseInt(m.group(2), 16), Integer.parseInt(m.group(3), 16), Integer.parseInt(m.group(4), 16), Integer.parseInt(m.group(1), 16));
                                    } else {
                                        throw new TextParseException("Invalid color. Valid format is #aarrggbb. Found: " + paramValue, lexer.yyline());
                                    }
                                }
                                break;
                            case "font":
                                try {
                                    fontId = Integer.parseInt(paramValue);

                                    FontTag ft = swf.getFont(fontId);
                                    if (ft == null) {
                                        throw new TextParseException("Font not found.", lexer.yyline());
                                    }

                                    font = (FontTag) ft;
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid font id - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "height":
                                try {
                                    textHeight = Integer.parseInt(paramValue);
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid font height - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "letterspacing":
                                try {
                                    letterSpacing = Integer.parseInt(paramValue);
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid font letter spacing - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "x":
                                try {
                                    x = Integer.parseInt(paramValue);
                                    currentX = x;
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid x position - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "y":
                                try {
                                    y = Integer.parseInt(paramValue);
                                    currentY = y;
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid y position - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "xmin":
                                try {
                                    textBounds.Xmin = Integer.parseInt(paramValue);
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid xmin position - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "xmax":
                                try {
                                    textBounds.Xmax = Integer.parseInt(paramValue);
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid xmax position - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "ymin":
                                try {
                                    textBounds.Ymin = Integer.parseInt(paramValue);
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid ymin position - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "ymax":
                                try {
                                    textBounds.Ymax = Integer.parseInt(paramValue);
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid ymax position - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "scalex":
                                try {
                                    textMatrix.scaleX = Integer.parseInt(paramValue);
                                    textMatrix.hasScale = true;
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid scalex value - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "scaley":
                                try {
                                    textMatrix.scaleY = Integer.parseInt(paramValue);
                                    textMatrix.hasScale = true;
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid scalex value - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "rotateskew0":
                                try {
                                    textMatrix.rotateSkew0 = Integer.parseInt(paramValue);
                                    textMatrix.hasRotate = true;
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid rotateskew0 value - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "rotateskew1":
                                try {
                                    textMatrix.rotateSkew1 = Integer.parseInt(paramValue);
                                    textMatrix.hasRotate = true;
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid rotateskew1 value - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "translatex":
                                try {
                                    textMatrix.translateX = Integer.parseInt(paramValue);
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid translatex value - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            case "translatey":
                                try {
                                    textMatrix.translateY = Integer.parseInt(paramValue);
                                } catch (NumberFormatException nfe) {
                                    throw new TextParseException("Invalid translatey value - number expected. Found: " + paramValue, lexer.yyline());
                                }
                                break;
                            default:
                                throw new TextParseException("Unrecognized parameter name: " + paramName, lexer.yyline());
                        }
                        break;
                    case TEXT:
                        String txt = (texts == null || textIdx >= texts.length) ? (String) s.values[0] : texts[textIdx++];
                        if (txt == null || (font == null && txt.isEmpty())) {
                            continue;
                        }

                        if (font == null) {
                            throw new TextParseException("Font not defined", lexer.yyline());
                        }

                        while (txt.charAt(0) == '\r' || txt.charAt(0) == '\n') {
                            txt = txt.substring(1);
                        }

                        while (txt.charAt(txt.length() - 1) == '\r' || txt.charAt(txt.length() - 1) == '\n') {
                            txt = txt.substring(0, txt.length() - 1);
                        }

                        StringBuilder txtSb = new StringBuilder();
                        for (int i = 0; i < txt.length(); i++) {
                            char c = txt.charAt(i);

                            if (!font.containsChar(c)) {
                                if (!missingCharHandler.handle(this, font, c)) {
                                    if (!missingCharHandler.getIgnoreMissingCharacters()) {
                                        return false;
                                    }
                                } else {
                                    return setFormattedText(missingCharHandler, formattedText, texts);
                                }
                            } else {
                                txtSb.append(c);
                            }
                        }

                        txt = txtSb.toString();

                        TEXTRECORD tr = new TEXTRECORD();
                        textRecords.add(tr);
                        if (fontId > -1) {
                            tr.fontId = fontId;
                            tr.textHeight = textHeight;
                            fontId = -1;
                            tr.styleFlagsHasFont = true;
                        }
                        if (getTextNum() == 1) {
                            if (color != null) {
                                tr.textColor = color;
                                tr.styleFlagsHasColor = true;
                                color = null;
                            }
                        } else if (colorA != null) {
                            tr.textColorA = colorA;
                            tr.styleFlagsHasColor = true;
                            colorA = null;
                        }
                        if (x != null) {
                            tr.xOffset = x;
                            tr.styleFlagsHasXOffset = true;
                            x = null;
                        }
                        if (y != null) {
                            tr.yOffset = y;
                            tr.styleFlagsHasYOffset = true;
                            y = null;
                        }
                        tr.glyphEntries = new ArrayList<>(txt.length());
                        for (int i = 0; i < txt.length(); i++) {
                            char c = txt.charAt(i);
                            Character nextChar = null;
                            if (i + 1 < txt.length()) {
                                nextChar = txt.charAt(i + 1);
                            }

                            GLYPHENTRY ge = new GLYPHENTRY();
                            ge.glyphIndex = font.charToGlyph(c);

                            int advance = getAdvance(font, ge.glyphIndex, textHeight, c, nextChar) + letterSpacing;
                            ge.glyphAdvance = advance;
                            tr.glyphEntries.add(ge);

                            currentX += advance;
                        }

                        if (currentX > maxX) {
                            maxX = currentX;
                        }
                        if (currentX < minX) {
                            minX = currentX;
                        }
                        break;
                }
            }

            setModified(true);
            this.textRecords = textRecords;
            this.textMatrix = textMatrix;
            this.textBounds = textBounds;
        } catch (IOException ex) {
            return false;
        } catch (TextParseException ex) {
            throw ex;
        }

        updateTextBounds();
        return true;
    }

    public static int getAdvance(FontTag font, int glyphIndex, int textHeight, char c, Character nextChar) {
        int advance;
        if (font.hasLayout()) {
            int kerningAdjustment = 0;
            if (nextChar != null) {
                kerningAdjustment = font.getCharKerningAdjustment(c, nextChar);
            }
            advance = (int) Math.round(((double) textHeight * (font.getGlyphAdvance(glyphIndex) + kerningAdjustment)) / (font.getDivider() * 1024.0));
        } else {
            String fontName = font.getSystemFontName();
            advance = (int) Math.round(SWF.unitDivisor * FontTag.getSystemFontAdvance(fontName, font.getFontStyle(), (int) (textHeight / SWF.unitDivisor), c, nextChar));
        }

        return advance;
    }

    public static int detectLetterSpacing(TEXTRECORD textRecord, FontTag font, int textHeight) {
        int totalLetterSpacing = 0;
        List<GLYPHENTRY> glyphEntries = textRecord.glyphEntries;
        for (int i = 0; i < glyphEntries.size(); i++) {
            GLYPHENTRY glyph = glyphEntries.get(i);
            GLYPHENTRY nextGlyph = null;
            if (i + 1 < glyphEntries.size()) {
                nextGlyph = glyphEntries.get(i + 1);
            }

            char c = font.glyphToChar(glyph.glyphIndex);
            Character nextChar = nextGlyph == null ? null : font.glyphToChar(nextGlyph.glyphIndex);
            int advance = getAdvance(font, glyph.glyphIndex, textHeight, c, nextChar);
            int letterSpacing = glyph.glyphAdvance - advance;
            totalLetterSpacing += letterSpacing;
        }

        return (int) Math.round(totalLetterSpacing / glyphEntries.size());
    }

    @Override
    public void getNeededCharacters(Set<Integer> needed) {
        for (TEXTRECORD tr : textRecords) {
            if (tr.styleFlagsHasFont) {
                needed.add(tr.fontId);
            }
        }
    }

    @Override
    public boolean replaceCharacter(int oldCharacterId, int newCharacterId) {
        boolean modified = false;
        for (TEXTRECORD tr : textRecords) {
            if (tr.fontId == oldCharacterId) {
                tr.fontId = newCharacterId;
                modified = true;
            }
        }
        if (modified) {
            setModified(true);
        }
        return modified;
    }

    @Override
    public boolean removeCharacter(int characterId) {
        boolean modified = false;
        for (TEXTRECORD tr : textRecords) {
            if (tr.fontId == characterId) {
                tr.styleFlagsHasFont = false;
                tr.fontId = 0;
                modified = true;
            }
        }
        if (modified) {
            setModified(true);
        }
        return modified;
    }

    @Override
    public int getUsedParameters() {
        return 0;
    }

    @Override
    public void toImage(int frame, int time, int ratio, RenderContext renderContext, SerializableImage image, SerializableImage fullImage, boolean isClip, Matrix transformation, Matrix strokeTransformation, Matrix absoluteTransformation, Matrix fullTransformation, ColorTransform colorTransform, double unzoom, boolean sameImage, ExportRectangle viewRect, boolean scaleStrokes, int drawMode) {
        staticTextToImage(swf, textRecords, getTextNum(), image, textMatrix, transformation, colorTransform);
        /*try {
         TextTag originalTag = (TextTag) getOriginalTag();
         if (isModified()) {
         originalTag.toImage(frame, time, ratio, renderContext, image, transformation, new ConstantColorColorTransform(0xFFC0C0C0));
         }
         staticTextToImage(swf, textRecords, getTextNum(), image, getTextMatrix(), transformation, new ConstantColorColorTransform(0xFF000000));
         } catch (InterruptedException | IOException ex) {
         Logger.getLogger(TextTag.class.getName()).log(Level.SEVERE, null, ex);
         }*/
    }

    @Override
    public void toSVG(SVGExporter exporter, int ratio, ColorTransform colorTransform, int level) {
        staticTextToSVG(swf, textRecords, getTextNum(), exporter, getRect(), textMatrix, colorTransform, 1);
    }

    @Override
    public void toHtmlCanvas(StringBuilder result, double unitDivisor) {
        staticTextToHtmlCanvas(unitDivisor, swf, textRecords, getTextNum(), result, textBounds, textMatrix, null);
    }
}
