package de.renew.gui.fs;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import de.uni_hamburg.fs.JavaConcept;
import de.uni_hamburg.fs.TypeSystem;

import CH.ifa.draw.figures.TextFigure;
import CH.ifa.draw.framework.ParentFigure;
import CH.ifa.draw.standard.AlignCommand;
import CH.ifa.draw.standard.ConnectionHandle;
import CH.ifa.draw.standard.RelativeLocator;
import CH.ifa.draw.util.ColorMap;
import CH.ifa.draw.util.ExtendedFont;
import CH.ifa.draw.util.Fontkit;
import de.renew.draw.storables.ontology.Locator;
import de.renew.draw.storables.ontology.StorableInput;
import de.renew.draw.ui.ontology.FigureHandle;
import de.renew.formalism.fs.ShadowConcept;
import de.renew.gui.NodeFigure;
import de.renew.simulatorontology.shadow.ShadowNet;
import de.renew.simulatorontology.shadow.ShadowNetElement;
import de.renew.util.StringUtil;


/**
 * The figure representing a concept.
 */
public class ConceptFigure extends TextFigure implements NodeFigure {
    private static final String STEREOTYPE_REGEXP = "^((?:<<|«)([a-zA-Z]*)(?:>>|»))\\n*";

    // group 0: whole match, group 1: comment, group 2: annotations, group 3: concept name
    private static final String CONCEPT_REGEXP =
        "(/\\*(?:[^*]|\\*[^/])*\\*/)?\n*((?:@[^\\n]*\n)*)\\n*([\\.\\:\\_a-zA-Z0-9-]*)";

    // group 0: whole match, group 1: comment, group 2: annotations, group 3: slot declaration
    //    private static final String SLOT_REGEXP =    "(/\\*(?:[^*]|\\*[^/])*\\*/)?\n*((?:@[^\\n]*\n)*)\\n*([a-zA-Z0-9-+_\\(\\)\\.]*\\s*:\\s*[a-zA-Z0-9-\\*]*)";
    private static final String SLOT_REGEXP =
        "\n*(/\\*(?:[^*]|\\*[^/])*\\*/)?\n*((?:@[^\\n]*\n)*)\\n*([^\n]+\n*)";

    /**
     * Logger of the ConceptFigure.
     */
    public static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(ConceptFigure.class);
    private static IsaConnection _fgIsaPrototype = new IsaConnection();
    private static IsaConnection _fgDisIsaPrototype = new IsaConnection(false);
    private static AssocConnection _fgAssocPrototype = new AssocConnection();

    /**
     * Concept string.
     */
    private String _conceptStr = "";

    /**
     * Property string.
     */
    private String _appropStr = "";

    /**
     * Index of the type.
     */
    private int _typeIndex = 0; // line index of type name

    /**
     * The shadow of this place figure.
     * Initially <code>null</code>, will be created
     * when needed.
     * <p>
     * This field is transient because its information
     * can be regenerated via <code>buildShadow(...)</code>.
     * </p>
     **/
    private transient ShadowConcept _shadow = null;

    /**
     * Text with documentation.
     */
    private String _textWithDoc;

    /**
     * Stereotype string.
     */
    private String _stereotype;

    /**
     * Documentation strings for slots.
     */
    private List<String> _slotDocumentation = new ArrayList<String>();

    /**
     * Documentation of the concept.
     */
    private String _conceptDocumentation;

    /**
     * Annotation strings for slots.
     */
    private List<String> _slotAnnotation = new ArrayList<String>();

    /**
     * Annotation of the concept.
     */
    private String _conceptAnnotation;

    /**
     * Constructor with default colors black and white.
     */
    public ConceptFigure() {
        super();
        setFrameColor(ColorMap.color("Black"));
        setFillColor(ColorMap.color("White"));
    }

    @Override
    public Vector<FigureHandle> handles() {
        Vector<FigureHandle> handles = super.handles();
        handles.addElement(new ConnectionHandle(this, RelativeLocator.north(), _fgDisIsaPrototype));
        handles.addElement(new ConnectionHandle(this, RelativeLocator.south(), _fgIsaPrototype));
        handles.addElement(new ConnectionHandle(this, RelativeLocator.east(), _fgAssocPrototype));


        //  handles.addElement(new ConnectionHandle(this, RelativeLocator.west(),
        //          fgAssocPrototype));
        // Add association handles for all lines that contain an attribute.
        String[] lines = getLines();
        Rectangle size = displayBox();
        Rectangle[] boxes = getLineBoxes(null);
        int yOffset = 0;
        boolean skippedClassName = false;
        if (boxes != null) {
            for (int i = 0; i < lines.length; i++) {
                Locator locator =
                    new RelativeLocator(0.0, (2 * yOffset + boxes[i].height) / 2.0 / size.height);
                yOffset += boxes[i].height;
                if ("".equals(lines[i])) {
                    // Ignore.
                } else if (isStereotype(lines[i])) {
                    // Ignore.
                } else if (isAnnotation(lines[i])) {
                    // Ignore.
                } else if (skippedClassName) {
                    // Split at colon.
                    String name = null;
                    String type = null;
                    StringTokenizer tokenizer = new StringTokenizer(lines[i], ":");
                    while (type == null && tokenizer.hasMoreTokens()) {
                        if (name == null) {
                            name = tokenizer.nextToken();
                        } else {
                            type = tokenizer.nextToken();
                        }
                    }

                    if (name != null && !tokenizer.hasMoreTokens()) {
                        // Ok, all tokens are used up. Clear spaces.
                        name = StringUtil.unspace(name);
                        if (type != null) {
                            type = StringUtil.unspace(type);
                        }

                        if (name != null) {
                            if (type == null) {
                                type = "UNKNOWN";
                            }

                            // Split preceeding modifiers.
                            String frontModifier = "";
                            if (name.startsWith("#") || name.startsWith("+")
                                || name.startsWith("-")) {
                                frontModifier = name.substring(0, 1);
                                name = StringUtil.unspace(name.substring(1));
                            }

                            String backModifiers = "";

                            // Split of modifier list enclosed in braces.
                            int bracePos = type.indexOf("{");
                            if (bracePos >= 0) {
                                backModifiers = " " + type.substring(bracePos);
                                type = StringUtil.unspace(type.substring(0, bracePos));
                            }

                            boolean isCollection = false;
                            while (type != null && type.endsWith("[]")) {
                                isCollection = true;
                                type = type.substring(0, type.length() - 2);
                            }

                            if (StringUtil.isNameOrEmpty(name, false)
                                && StringUtil.isNameOrEmpty(type, true)) {
                                // This seems to be an attribute declaration, not a
                                // method call.
                                // Reattach the modifiers that were previously removed.
                                type = "." + type;
                                if (!"".equals(frontModifier)) {
                                    // How is the accesibility indicated for an association?
                                    // name=frontModifier+" "+name;
                                    // Use the field's accesibility for diplaying the class.
                                    // Is there a better solution?
                                    type = frontModifier + type;
                                }
                                name = StringUtil.unspace(name + backModifiers);


                                // Create the handle.
                                handles.addElement(
                                    new AssociationHandle(
                                        this, i, name, type, isCollection, locator,
                                        _fgAssocPrototype));
                            }
                        }
                    }
                } else {
                    skippedClassName = true;
                    // Line should be class name. Skip.
                }
            }
        }
        return handles;
    }

    /* hack for setting ConceptFigures to editable: */
    @Override
    public void read(StorableInput dr) throws IOException {
        super.read(dr);
        setReadOnly(false);


        /* hack for getting rid of parent PartitionFigure: */
        setParent(null);
    }

    private static boolean isStereotype(String str) {
        return str.startsWith("<<") || str.startsWith("«");
    }

    private static boolean isAnnotation(String str) {
        return str.startsWith("@");
    }

    /**
     * Retrieve the stereotype.
     *
     * @return stereotype retrieved
     */
    public String getStereotype() {
        Pattern pattern = Pattern.compile(STEREOTYPE_REGEXP);
        Matcher stereotypeMatcher = pattern.matcher(_stereotype);
        if (_stereotype.length() > 0) {
            if (stereotypeMatcher.find()) {
                return stereotypeMatcher.group(2);
            } else {
                LOGGER.error(
                    "The concept figure " + _conceptStr
                        + " does not follow the syntax rules in its stereotype specification: "
                        + _stereotype + ".");
                return null;
            }
        }
        return _stereotype;
    }

    @Override
    protected void drawLine(Graphics g, int i) {
        if (!"".equals(getLine(i))) {
            super.drawLine(g, i);
        } else {
            int y = getLineBox(g, i).y;
            int width = displayBox().width;
            g.drawLine(-5, y + 1, width - 6, y + 1);
        }
    }

    @Override
    protected String getLine(int i) {
        String line = super.getLine(i);
        if (line.startsWith("\\") || line.startsWith("_")) {
            return line.substring(1);
        }
        return line;
    }

    @Override
    protected Font getLineFont(int i) {
        String line = super.getLine(i);
        Font font = super.getLineFont(i); //NOTICEsignature
        int fontStyle = font.getStyle();
        if (i == _typeIndex) {
            fontStyle |= Font.BOLD;
        }
        if (line.length() > 0) { // should be, otherwise getLineFont is not called.
            char styleChar = line.charAt(0);
            switch (styleChar) {
                case '\\':
                    fontStyle |= Font.ITALIC;
                    break;
                case '_':
                    fontStyle |= ExtendedFont.UNDERLINED;
            }
        }
        return Fontkit.getFont(font.getName(), fontStyle, font.getSize());
    }

    @Override
    protected int getLineAlignment(int i) {
        if (i <= _typeIndex) {
            return AlignCommand.CENTERS;
        }
        return super.getLineAlignment(i); //NOTICEsignature
    }

    @Override
    protected Dimension getLineDimension(int i, Graphics g) {
        if ("".equals(getLine(i))) {
            return new Dimension(0, 3);
        }
        return super.getLineDimension(i, g);
    }

    @Override
    public void setText(String text) {
        if (text.equals(_textWithDoc)) {
            return;
        }
        _typeIndex = 0;
        _appropStr = "";

        // strip javadoc comments from the shown text
        _textWithDoc = text;
        _conceptStr = text.trim();
        _stereotype = "";
        _conceptDocumentation = "";
        _slotDocumentation = new ArrayList<String>();
        _conceptAnnotation = "";
        _slotAnnotation = new ArrayList<String>();
        String textWithDocumentation = _textWithDoc;

        String implementationDescription = generateImplementationDescription();
        if (implementationDescription != null) {
            textWithDocumentation = implementationDescription;
        }

        // Match stereotype at beginning
        textWithDocumentation = matchStereotype(textWithDocumentation);

        // Match the concept declaration
        textWithDocumentation = matchConceptDeclaration(textWithDocumentation);

        if (implementationDescription != null) {
            _appropStr = textWithDocumentation;
            _textWithDoc = text = implementationDescription;
        } else {
            // Match each slot
            matchSlots(textWithDocumentation);
        }

        willChange();
        basicSetText(_appropStr.length() > 0 ? _conceptStr + "\n\n" + _appropStr : _conceptStr);
        changed();
        LOGGER.debug("Concept name: \"" + _conceptStr + "\", approp: \"" + _appropStr + "\"");
    }

    private String generateImplementationDescription() {
        if (_conceptStr.trim().length() > 0) {

            int level = JavaConcept.getVisibilityLevel(_conceptStr.charAt(0));
            if (level >= 0) {
                _conceptStr = _conceptStr.substring(1);
            }
            boolean wantPackages = _conceptStr.startsWith(".");
            if (wantPackages) {
                _conceptStr = _conceptStr.substring(1);
            }
            TypeSystem ts = TypeSystem.instance();
            try {
                Class<?> clazz = ts.getJavaClass(_conceptStr);
                String expandedText;
                String[] wellKnown;
                if (wantPackages) {
                    wellKnown = new String[0];
                } else {
                    wellKnown = JavaConcept.getWellKnownPackages(clazz);
                }
                if (level >= 0) {
                    expandedText =
                        JavaConcept.getImplementationDescription(clazz, level, wellKnown);
                } else {
                    expandedText = ts.getJavaConcept(clazz).getClassDescription(wellKnown);
                }
                return expandedText;
            } catch (Throwable t) {
                return null;
            }
        }
        return null;
    }

    private void matchSlots(String textWithDocumentation) {
        Pattern slotPattern = Pattern.compile(SLOT_REGEXP);
        Matcher slotMatcher = slotPattern.matcher(textWithDocumentation);
        List<String> slots = new ArrayList<String>();
        while (slotMatcher.find()) {
            String comment = slotMatcher.group(1);
            String strippedComment = "";
            if (comment != null) {
                strippedComment =
                    comment.substring("/**".length(), comment.length() - "*/\n".length()).trim();
            }
            _slotDocumentation.add(strippedComment);
            String annotations = slotMatcher.group(2);
            _slotAnnotation.add((annotations != null) ? annotations : "");
            String slot = slotMatcher.group(3);
            slots.add(slot);
        }
        _appropStr = String.join("", slots);
    }

    private String matchConceptDeclaration(String textWithDocumentation) {
        Pattern conceptPattern = Pattern.compile(CONCEPT_REGEXP);
        Matcher conceptMatcher = conceptPattern.matcher(textWithDocumentation);
        if (conceptMatcher.find()) {
            String comment = conceptMatcher.group(1);
            String strippedComment = "";
            if (comment != null) {
                strippedComment =
                    comment.substring("/**".length(), comment.length() - "*/\n".length()).trim();
            }
            _conceptDocumentation = strippedComment;
            String annotations = conceptMatcher.group(2);
            _conceptAnnotation = (annotations != null) ? annotations : "";

            _conceptStr = _stereotype.length() > 0 ? _stereotype + "\n" + conceptMatcher.group(3)
                : conceptMatcher.group(3);

            // continue with the substring that begins with the slot declarations
            textWithDocumentation = textWithDocumentation.substring(conceptMatcher.end());
        }
        return textWithDocumentation;
    }

    private String matchStereotype(String textWithDocumentation) {
        Pattern stereotypePattern = Pattern.compile(STEREOTYPE_REGEXP);
        Matcher stereotypeMatcher = stereotypePattern.matcher(textWithDocumentation);
        if (stereotypeMatcher.find()) {
            _stereotype = stereotypeMatcher.group(1);
            if (_stereotype.length() > 0) {
                _typeIndex = 1;
            }
            textWithDocumentation = textWithDocumentation.substring(stereotypeMatcher.end());
        }
        return textWithDocumentation;
    }

    @Override
    protected void internalSetText(String newText) {
        // A bit ugly, but we can't use an initializer here,
        // as this method is called from the parent-constructor
        // when the initializer has not been run.
        if (_textWithDoc == null) {
            _textWithDoc = newText;
        }
        super.internalSetTextHiddenParts(_textWithDoc, newText);
    }

    @Override
    public Rectangle displayBox() {
        Rectangle box = super.displayBox();
        return new Rectangle(box.x - 5, box.y, box.width + 10, box.height + 1);
    }

    /** Build a shadow in the given shadow net.
      *  This shadow is stored as well as returned.
      */
    @Override
    public ShadowNetElement buildShadow(ShadowNet net) {
        _shadow = new ShadowConcept(
            net, _conceptStr, _appropStr, _conceptDocumentation, _slotDocumentation,
            _conceptAnnotation, _slotAnnotation, _stereotype);
        _shadow.setContext(this);
        LOGGER.debug("shadow for concept " + _conceptStr + " created!");
        return _shadow;
    }

    /** Get the associated shadow, if any.
     */
    @Override
    public ShadowNetElement getShadow() {
        return _shadow;
    }

    /**
     * Retrieve the name of the figure.
     *
     * @return name of the figure
     */
    public String getName() {
        //        if(getStereotype().length()>0) {
        //            return getStereotype()+"\n"+conceptStr;
        //        } else {
        return _conceptStr;
        //        }
    }

    @Override
    public void release() {
        super.release();
        if (_shadow != null) {
            _shadow.discard();
        }
    }

    @Override
    public boolean canBeParent(ParentFigure figure) {
        //if (figure instanceof PartitionFigure)
        //  return super.canBeParent(figure);
        // only PartitionFigures can be parents of ConceptFigures!
        //return false;
        return figure == null;
    }
}
