package cnp.ew.grid;

import java.util.*;
import java.awt.*;
import java.io.*;

import cnp.ew.displayer.*;
import cnp.ew.converter.*;
import cnp.ew.lightweight.*;
import cnp.ew.properties.*;
import cnp.ew.util.*;

public class CpGridCell
{
    public static CpGridBorderDisplayer defaultBorderDisplayer = new CpGridBorderDisplayer();

    static final int NO_BORDER = 0;
    static final int BOTTOM_BORDER = 1;
    static final int LEFT_BORDER = 2;
    static final int RIGHT_BORDER = 3;
    static final int DOUBLE_BOTTOM_BORDER = 4;
    static final int THICK_BOTTOM_BORDER = 5;
    static final int TOP_BOTTOM_BORDER = 6;
    static final int TOP_DOUBLE_BOTTOM_BORDER = 7;
    static final int TOP_THICK_BOTTOM_BORDER = 8;
    static final int GRID_BORDER = 9;
    static final int OUTLINE_BORDER = 10;
    static final int THICK_OUTLINE_BORDER = 11;

    Object object;
    CpType type, setType; // Set type (set specifically by the user) is used to try to parse the objectAsString.  If it doesn't work, we go to normal parsing to guess the type
    String stringBeingEdited;
    // THIS IS WRONG.  Wanted to use CpDisplayer and test capabilities, but it is so tedius for setFont, setAlign, etc.
    // Right now only works for GSD things.
    CpGeneralStringDisplayer displayer;
    CpGridBorderDisplayer borderDisplayer = defaultBorderDisplayer;
    Hashtable converterDict;

    boolean isFormula = false;
    String formulaSource;
    CpFormulaExpression formula;
    Vector dependents = new Vector();
    Vector dependsOn = new Vector();
    boolean calced, willBeCalced;


    public CpGridCell()
    {
        super();
        type = CpBasicType.STRING;
        displayer = (CpGeneralStringDisplayer)type.getDefaultDisplayer();
        // THis is bad.  See comment below.
        displayer.setFont(CpFonts.dialogPlainTen());
        displayer.selectionBackColor = null;

    }

    /************************** GET/SET **************************/

    // Sent as user is typing
    public void setStringBeingEdited(String s)
    {
        stringBeingEdited = s;
    }

    public String getTextForEditing()
    {
        if (formulaSource != null) {
            return formulaSource;
        } else {
            return getObjectAsString();
        }
    }

    public boolean inEditMode()
    {
        return (stringBeingEdited != null);
    }

    public String getObjectAsString()
    {
        if (object == null) {
            return "";
        }
        if (type.getDefaultConverter() != null) {
            return type.getDefaultConverter().convert(object);
        }
        return null;
    }

    public Object getObject()
    {
        return object;
    }

    public double getNumValue()
    {
        double cellNumValue;
        if (getObject() instanceof Number) {
            return ((Double)getObject()).doubleValue();
        } else {
            return 0;
        }
    }

    public void acceptText(CpGridModel gridModel)
    {
        setObjectAsString(stringBeingEdited, gridModel);
        stringBeingEdited = null;

    }

    /*************** API ****************/

    public CpType getType()
    {
        return type;
    }

    public String getFormatString()
    {
        return ((CpFormattedToStringConverter)getConverter()).getFormatString();
    }

    public boolean isBold()
    {
        return displayer.getFont().isBold();
    }

    public boolean isItalic()
    {
        return displayer.getFont().isItalic();
    }

    public String getFontName()
    {
        return displayer.getFont().getName();
    }

    public int getPointSize()
    {
        return displayer.getFont().getSize();
    }

    public int getHorizontalAlignment()
    {
        return displayer.getHorizontalAlignment();
    }

    public int getVerticalAlignment()
    {
        return displayer.getVerticalAlignment();
    }

    public boolean getShouldWrap()
    {
        return displayer.getShouldWrap();
    }



    public CpToStringConverter getConverter()
    {
        if (converterDict == null || !converterDict.containsKey(type)) {
            return type.getDefaultConverter();
        } else {
            return (CpToStringConverter)converterDict.get(type);
        }
    }

    public void setConverter(CpToStringConverter converter)
    {
        if (converterDict == null) {
            converterDict = new Hashtable();
        }
        converterDict.put(type, converter);
    }
    // WE SHOULD NOT USE GENERAL STRING DISPLAYER.  WE NEED EITHER AN INTERFACE OR A BUNCH OF INTERFACE "CAPABILITIES" (uses fonts, alignable, modelable, etc.)
    public void getMyOwnGsd()
    {
        // Terrible hack.  Changed type.getDD to Num.getD and String.getD to cover the TWO displayer we return in basic type.  Outch.
        if (displayer == CpBasicType.STRING.getDefaultDisplayer()) {
            try {
                displayer = (CpGeneralStringDisplayer)type.getDefaultDisplayer().getClass().newInstance();
            } catch (Exception e) { System.out.println("Exception e = " + e); e.printStackTrace(); }

            // This is bad too, but we want to assume the displayer has a font, rather than always testing null
            displayer.setFont(CpFonts.dialogPlainTen());
            displayer.selectionBackColor = null;
        }
    }

    public void setType(CpType newType, CpGridModel model)
    {
        type = setType = newType;
        if (object != null) {
            setObjectAsString(getObjectAsString(), model);
        }
    }

	public void setFormatString(String format)
	{
        if (converterDict == null || !converterDict.containsKey(type)) {
            try {
                setConverter((CpToStringConverter)type.getDefaultConverter().getClass().newInstance());
            } catch (Exception e) {}
        }
        getConverter().setFormatString(format);
    }


    public void setHorizontalAlignment(int alignment)
    {
        getMyOwnGsd();
        displayer.setHorizontalAlignment(alignment);
    }

    public void setVerticalAlignment(int alignment)
    {
        getMyOwnGsd();
        displayer.setVerticalAlignment(alignment);
    }

    public void setForecolor(Color color)
    {
        getMyOwnGsd();
        displayer.foreColor = color;
    }

    public void setBackcolor(Color color)
    {
        getMyOwnGsd();
        displayer.backColor = color;
    }

    public void setFontName(String name)
    {
        getMyOwnGsd();
        displayer.setFont(new Font(name, displayer.getFont().getStyle(), displayer.getFont().getSize()));
    }

    public void setFontSize(int size)
    {
        getMyOwnGsd();
        displayer.setFont(new Font(displayer.getFont().getName(), displayer.getFont().getStyle(), size));
    }

    public void setBold(boolean bool)
    {
        getMyOwnGsd();
        int style;
        if (bool) {
            style = displayer.getFont().getStyle() | Font.BOLD;
        } else {
            style = displayer.getFont().getStyle() & (Font.ITALIC | Font.PLAIN);
        }
        displayer.setFont(new Font(displayer.getFont().getName(), style, displayer.getFont().getSize()));
    }

    public void setItalic(boolean bool)
    {
        getMyOwnGsd();
        int style;
        if (bool) {
            style = displayer.getFont().getStyle() | Font.ITALIC;
        } else {
            style = displayer.getFont().getStyle() & (Font.BOLD | Font.PLAIN);
        }
        displayer.setFont(new Font(displayer.getFont().getName(), style, displayer.getFont().getSize()));
    }

    public void setShouldWrap(boolean bool)
    {
        getMyOwnGsd();
        displayer.setShouldWrap(bool);
    }

    public void setBorder(int side, int thickness, CpLightweightComponent c)
    {
        int leftThickness = borderDisplayer.getLeftBorderThickness(c);
        int rightThickness = borderDisplayer.getRightBorderThickness(c);
        int topThickness = borderDisplayer.getTopBorderThickness(c);
        int bottomThickness = borderDisplayer.getBottomBorderThickness(c);
        switch (side) {
            case CpGridBorderDisplayer.LEFT : { leftThickness = thickness; break; }
            case CpGridBorderDisplayer.RIGHT : { rightThickness = thickness; break; }
            case CpGridBorderDisplayer.TOP : { topThickness = thickness; break; }
            case CpGridBorderDisplayer.BOTTOM : { bottomThickness = thickness; break; }
        }
        borderDisplayer = new CpGridBorderDisplayer(leftThickness, topThickness, rightThickness, bottomThickness);
    }

    public void setBorderType(int borderType)
    {
        int leftThickness = 0, rightThickness = 0, topThickness = 0, bottomThickness = 0;
        switch (borderType) {
            case BOTTOM_BORDER : { bottomThickness = 1; break; }
            case LEFT_BORDER : { leftThickness = 1; break; }
            case RIGHT_BORDER : { rightThickness = 1; break; }
            case DOUBLE_BOTTOM_BORDER : { bottomThickness = -3; break; }
            case THICK_BOTTOM_BORDER : { bottomThickness = 3; break; }
            case TOP_BOTTOM_BORDER : { topThickness = 1; bottomThickness = 1; break; }
            case TOP_DOUBLE_BOTTOM_BORDER : { topThickness = 1; bottomThickness = -3; break; }
            case TOP_THICK_BOTTOM_BORDER : { topThickness = 1; bottomThickness = 3; break; }
            case GRID_BORDER : { bottomThickness = 1; leftThickness = 1; rightThickness = 1; topThickness = 1; break; }
        }
        borderDisplayer = new CpGridBorderDisplayer(leftThickness, topThickness, rightThickness, bottomThickness);
    }

    /************************* PAINTING ***********************/

    public Object getDisplayer()
    {
        if (displayer == null) { // Only in the case of an lc
            return object;
        }
        return displayer;
    }

    public void paintIn(CpLightweightComponent c, Graphics g, int x, int y, int w, int h, boolean isSelected)
    {
        Insets insets = borderDisplayer.insets(c);
        Rectangle inner = new Rectangle(x + insets.left + 1, y + insets.top + 1, w - insets.left - insets.right - 2, h - insets.top - insets.bottom - 2);

        Object displayer = getDisplayer();
        if (stringBeingEdited != null && displayer instanceof CpGeneralStringDisplayer) {
            ((CpGeneralStringDisplayer)displayer).setModel(stringBeingEdited);
            ((CpGeneralStringDisplayer)displayer).paintIn(c, g, inner);
        } else {
            if (displayer instanceof CpModelDisplayable) {
                CpToStringConverter conv = getConverter();
                if (conv != null) {
                    ((CpModelDisplayable)displayer).setModel(conv.convert(object));
                } else {
                    ((CpModelDisplayable)displayer).setModel(object);
                }

            }
            if (displayer instanceof CpSelectable) {
                ((CpSelectable)displayer).setIsSelected(isSelected);
            }
            if (displayer instanceof CpDisplayable) {
                ((CpDisplayable)displayer).paintIn(c, g, inner);
            }
        }
        borderDisplayer.paintIn(c, g, x, y, w, h);
    }

    /****************************** MAIN INPUT, PARSING, RECALC ***********************/

    // This will parse the object, guess the type if it is not already set, and then call setObjectAndType

    public void setObjectAsString(String theString, CpGridModel gridModel)
    {
        Object parsed;

        isFormula = false;

        if (setType != null && (parsed = setType.parseString(theString)) != null) {
           // System.out.println("In set object as string, setType = " + setType + " + parsed = " + parsed);
            setObjectAndType(parsed, setType, gridModel);
            return;
        }

        // Now try Date and Number
        if ((parsed = CpBasicType.DATE.parseString(theString)) != null) {
            setObjectAndType(parsed, CpBasicType.DATE, gridModel);
            return;
        }

        if ((parsed = CpBasicType.NUMBER.parseString(theString)) != null) {
          //  System.out.println("Trying number");
            setObjectAndType(parsed, CpBasicType.NUMBER, gridModel);
            return;
        }
           // System.out.println("Settled for string");
        // Assume string
        isFormula = ((theString.length() > 0) && (theString.charAt(0) == '='));
        setObjectAndType(theString, CpBasicType.STRING, gridModel);
    }

    void setObjectAndType(Object o, CpType newType, CpGridModel gridModel)
    {
        type = newType;


        object = o;

        CpGridCell cell;

                //System.out.println("Going into depencey stuff");
        // First, tell all cells I WAS dependendent on that I'm not anymore...
        for (int d = 0; d < dependsOn.size(); d++) {
            cell = (CpGridCell)dependsOn.elementAt(d);
            cell.dependents.removeElement(this);
        }
        // Clear formula values.  (Will be rebuilt if this is a new formula)
        dependsOn = new Vector();
        formulaSource = null;
        formula = null;
        // Is this a formula?
        if (isFormula) {
            formulaSource = (String)object;
            formula = parseFormula(gridModel, formulaSource, dependsOn);
            if (formula == null) {
                object = "#SYNTAX ERROR";
                type = CpBasicType.STRING;
            } else {
                type = formula.getType();
                // Inform cells I depend on
                for (int d = 0; d < dependsOn.size(); d++) {
                    cell = (CpGridCell)dependsOn.elementAt(d);
                    if (!cell.dependents.contains(this)) {
                        cell.dependents.addElement(this);
                    }
                }
                calced = false;
                object = recalc(gridModel);
            }
        }
        // Displayer stuff is total disaster
        if (type == CpBasicType.NUMBER) {
            setHorizontalAlignment(CpAlignable.ALIGN_RIGHT);
        }
        // IF anyone is dependent on me, tell them to recalc...
        // First mark whole dependent tree with calced = false and willBeCalced to true
        // Then go through tree, and recalc()
        if (setDependentsCalced()) {
            object = "#CIRCULAR ERROR";
            type = CpBasicType.STRING;

            for (int d = 0; d < dependsOn.size(); d++) {
                cell = (CpGridCell)dependsOn.elementAt(d);
                cell.dependents.removeElement(this);
            }
            dependsOn = new Vector();
            dependents = new Vector();
        } else {
            for (int d = 0; d < dependents.size(); d++) {
                cell = (CpGridCell)dependents.elementAt(d);
                cell.recalcAndUpdate(gridModel);
            }
        }
    }

    public boolean setDependentsCalced()
    {
       // System.out.println("I am " + getObjectAsString());
        for (int d = 0; d < dependents.size(); d++) {
            CpGridCell cell = (CpGridCell)dependents.elementAt(d);
         //   System.out.println("In dep calced, d = " + d);
          //  System.out.println(" dependent = " + cell.getObjectAsString());
            if (!cell.calced) {
              //  System.out.println("Circ error!");
                return true;
            }
            cell.calced = false;
            boolean result = cell.setDependentsCalced();
            if (result) {
                return true;
            }
        }
        return false;
    }

    public void recalcAndUpdate(CpGridModel gridModel)
    {
        CpGridCell cell;

        object = recalc(gridModel);
        for (int d = 0; d < dependents.size(); d++) {
            cell = (CpGridCell)dependents.elementAt(d);
            cell.calced = false;
            cell.recalcAndUpdate(gridModel);
        }
    }

    Object recalc(CpGridModel gridModel)
    {
        if (calced) {
            //System.out.println("CIRCULAR ERROR!!!!!!!!!!!!!!!!!!");
            return "";
        }
        calced = true;
        // if any dependsOn are will be calced, recalc() them first.
        if (formula != null) {
            return formula.value(gridModel);
        } else {
            return object;
        }
    }

    // Maybe these could be bundled into a parse context object
    int curToken;
    Vector tokens;
    CpGridModel model;

    CpFormulaExpression parseFormula(CpGridModel gridModel, String s, Vector depsOn)
    {
        StringTokenizer st = new StringTokenizer(s, "\t\n\r+-*/().,[]{}@#$%^&:;<>?~=", true);
        tokens = new Vector();
        while (st.hasMoreTokens()) {
            String t = st.nextToken().trim();
            if (t.length() > 0) {
                tokens.addElement(t);
            }
        }

        curToken = 1; // Skip the equals
        model = gridModel;
        try {
            return parseExpression(true);
        } catch (CpSyntaxException e) {
            //System.out.println("Syntax error, curToken = " + curToken + " = " + tokens.elementAt(curToken));
            //System.out.println("Exception: " + e.toString());
            return null;
        }
//        return null;
        // This is terrible
//        Point cell1 = parseCellDescription(s.substring(1, 3));
  //      Point cell2 = parseCellDescription(s.substring(4, 6));
    //    depsOn.addElement(gridModel.getCell(cell1));
//        depsOn.addElement(gridModel.getCell(cell2));
  //      return new CpFormulaBinaryOp(s.substring(3, 4), new CpFormulaCell(cell1.x, cell1.y), new CpFormulaCell(cell2.x, cell2.y));
    }

    CpFormulaExpression parseExpression(boolean mustBe) throws CpSyntaxException
    {
        CpFormulaCell cellRef;
        String functionName;
        CpFormulaExpression arg1;

        if ((cellRef = parseCellRef(false)) != null) {
            arg1 = cellRef;
        } else {
            functionName = parseFunctionName(true);
            parseLiteral("(", true);
            Vector parameters = new Vector();
            while (true) {
                parameters.addElement(parseParameter(true));
                if (parseLiteral(")", false) != null) {
                    break;
                }
                parseLiteral(",", true);
            }
            arg1 = new CpFormulaFunction(functionName, parameters);
        }
        if (curToken >= tokens.size()) {
            return arg1;
        } else {
            return new CpFormulaBinaryOp(parseBinaryOp(true), arg1, parseExpression(true));
        }
    }

    CpFormulaCell parseCellRef(boolean mustBe) throws CpSyntaxException
    {
        boolean lookingForNumbers = false;
        String numString = "", letterString = "";

        int asciiA = 65;
        int asciiZ = 90;
        //System.out.println("Trying to parse Cell ref, curToken = " + tokens.elementAt(curToken));
        String token = getCurToken();
        for (int i = 0; i < token.length(); i++) {
            char c = token.charAt(i);
            if (lookingForNumbers) {
                if (!Character.isDigit(c)) {
                    if (mustBe) {
                        throw new CpSyntaxException("Parsing Cell Ref from token: " + token + ", expected a number, got " + c);
                    } else {
                        return null;
                    }
                }
                numString += c;
            } else {
                if (Character.isDigit(c)) {
                    lookingForNumbers = true;
                    numString += c;
                } else {
                    c = Character.toUpperCase(c);
                    if ((int)c < asciiA || (int)c > asciiZ) {
                        if (mustBe) {
                            throw new CpSyntaxException("Parsing Cell Ref from token: " + token + ", invalid character: " + c);
                        } else {
                            return null;
                        }
                    } else {
                        letterString += c;
                    }
                }
            }
        }
        if (numString.equals("")) {
            if (mustBe) {
                throw new CpSyntaxException("No number in cell ref: " + token);
            } else {
                return null;
            }
        }

     //   System.out.println("Parsed cell ref, letterString: " + letterString + " numString: " + numString);

        int column = 0;
        for (int i = 0; i < letterString.length(); i++) {
            column += (int)(((int)letterString.charAt(i) - asciiA + 1) * Math.pow(26, letterString.length() - i - 1));
        }
        if (letterString.length() == 1) { // Weird thing about alphabet. A = 0, but AA = 27.  In one case A is the 0 value, in the second it is the 1 value
            column--;
        }
        int row = (new Integer(numString)).intValue() - 1;
        if (letterString.length() == 0 || column >= model.getSizeInCells().width || row >= model.getSizeInCells().height) {
            //System.out.println("cell ref outta range, token = " + token);
            if (mustBe) {
                throw new CpSyntaxException("Cell reference out of range: " + token);
            } else {
                return null;
            }
        }
        //System.out.println("Got cell ref " + column + "," + row);
        curToken++;
        if (!dependsOn.contains(model.getCell(new Point(column, row)))) {
        //    System.out.println("ADDING DEPENDSON = " + new Point(column, row));
            dependsOn.addElement(model.getCell(new Point(column, row)));
        }
        return new CpFormulaCell(column, row);
    }

    String parseFunctionName(boolean mustBe) throws CpSyntaxException
    {
        //System.out.println("Trying to parse function, curToken = " + tokens.elementAt(curToken));
        String token = getCurToken().toLowerCase();
        if (token.equals("sum") || token.equals("average")) {
            curToken++;
            return token;
        } else {
            if (mustBe) {
                throw new CpSyntaxException("Looking for function name, got: " + token);
            } else {
                return null;
            }
        }
    }

    String parseLiteral(String literal, boolean mustBe) throws CpSyntaxException
    {
        //System.out.println("Trying to parse literal, curToken = " + tokens.elementAt(curToken));
        String token = getCurToken();
        if (token.equals(literal)) {
            curToken++;
            return token;
        } else {
            if (mustBe) {
                throw new CpSyntaxException("Looking for literal: " + literal + ", got: " + token);
            } else {
                return null;
            }
        }
    }

    String parseBinaryOp(boolean mustBe) throws CpSyntaxException
    {
        //System.out.println("Trying to parse binary op, curToken = " + tokens.elementAt(curToken));
        String token = getCurToken();
        String ops = "+-*/";
        if (ops.indexOf(token) >= 0) {
            curToken++;
            return token;
        } else {
            if (mustBe) {
                throw new CpSyntaxException("Looking for binary op, got: " + token);
            } else {
                return null;
            }
        }
    }

    CpFormulaExpression parseParameter(boolean mustBe) throws CpSyntaxException
    {
        CpFormulaCell cellRef;
        cellRef = parseCellRef(true);
        if (parseLiteral(":", false) != null) {
            CpFormulaCell cellRef2 = parseCellRef(true);
            for (int c = cellRef.cell.x; c <= cellRef2.cell.x; c++) {
                for (int r = cellRef.cell.y; r <= cellRef2.cell.y; r++) {
                    if (!dependsOn.contains(model.getCell(new Point(c, r)))) {
                      //  System.out.println("ADDING DEPENDSON = " + new Point(c, r));
                        dependsOn.addElement(model.getCell(new Point(c, r)));
                    }
                }
            }
            return new CpFormulaRange(cellRef, cellRef2);
        } else {
            return cellRef;
        }
    }

    String getCurToken()
    {
        return (String)tokens.elementAt(curToken);
    }




    Point parseCellDescription(String s)
    {
        int column = (int)s.charAt(0);
        int asciiA = 65;
        Point p = new Point(column - 65, (new Integer(s.substring(1, 2))).intValue() - 1);
        return p;
    }


    /******************** FILE I/O *****************************/

    public CpGridCell(RandomAccessFile file, CpGridModel gridModel) throws IOException
    {
        super();
        //System.out.println("Reading cell");
        object = file.readUTF();
        //System.out.println("object = " + object);
        String s = file.readUTF();
        if (s.equals(" ")) {
            formulaSource = null;
        } else {
            formulaSource = s;
            //System.out.println("got a formula: " + s);
            formula = parseFormula(gridModel, formulaSource, dependsOn);
        }
        if (file.readBoolean()) {
            //System.out.println("Reading displayer");
          //  displayer = new CpFormatSpec(file);
        }
        int size = file.readInt();
        //System.out.println("Reading dependents, size = " + size);
        for (int i = 0; i < size; i++) {
            int x = file.readInt();
            int y = file.readInt();
            dependents.addElement(new Point(x, y));
        }
        size = file.readInt();
        //System.out.println("Reading depnedsOn, size = " + size);
        for (int i = 0; i < size; i++) {
            int x = file.readInt();
            int y = file.readInt();
            dependsOn.addElement(new Point(x, y));
        }
    }

    public void writeTo(RandomAccessFile file) throws IOException
    {
        //System.out.println("Writing cell, object = " + object);
        file.writeUTF((String)object);

        if (formulaSource == null) {
            file.writeUTF(" ");
        } else {
            file.writeUTF(formulaSource);
        }
     /*   if (displayer == defaultDisplayer) {
            //System.out.println("Writing displayer");
            file.writeBoolean(true);
            displayer.writeTo(file);
        } else {
            file.writeBoolean(false);
        }
        */
        file.writeInt(dependents.size());
        for (int i = 0; i < dependents.size(); i++) {
            Point p = (Point)dependents.elementAt(i);
            file.writeInt(p.x);
            file.writeInt(p.y);
        }
        file.writeInt(dependsOn.size());
        for (int i = 0; i < dependsOn.size(); i++) {
            Point p = (Point)dependsOn.elementAt(i);
            file.writeInt(p.x);
            file.writeInt(p.y);
        }
    }
}