package svgconverter;

import java.awt.Color;
import java.util.*;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;

import javax.swing.JFrame;

public class SVGDrawer extends JFrame {
    static int docMinX = 0;
    static int docMinY = 0;
    static int docMaxX = 0;
    static int docMaxY = 0;
    static double docInvSizeX = 0;
    static double docInvSizeY = 0;
    
    static int sizeX = 800;
    static int sizeY = 800;
//    static int sizeX = 500;
//    static int sizeY = 1000;
    double[] maxVals = {0,0};

    Collection<Path> paths;
//    final AffineTransform FLIP_X_COORDINATE;

    public SVGDrawer(Collection<Path> paths) {
        super("SVG Drawer");
        this.paths = paths;
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(sizeX, sizeY);
        setVisible(true);
    }

    public void testMax(int d, double val) {
        double valAbs = Math.abs(val);
        if(valAbs > this.maxVals[d])
            this.maxVals[d] = valAbs;
    }

    public double[] evalBezier(double[] controlPoints, double t) {
        double[] result = new double[2];
        double tinv = 1.0 - t;
        for(int i = 0; i < 2; i++) {
            result[i]  = tinv*tinv*tinv * controlPoints[0 + i];
            result[i] += 3 * tinv*tinv * t * controlPoints[2 + i];
            result[i] += 3 * tinv * t * t * controlPoints[4 + i];
            result[i] += t * t * t * controlPoints[6 + i];
        }
        return result;
    }
/*
    public int compressCoord(double val, double low, double high, int bits) {
        int res;
        int steps = (int)Math.pow(2, bits);

        res = (int)(Math.round(((val - low) / (high - low)) * (steps - 1)));
        if(res < 0) {
//            System.out.println("Warning: res = " + res + " < " + " 0 " + " low:" + low + "  high: " + high + " val: " + val + " bits: " + bits);
            res = 0;
            rangeErrorsLow = true;
        }
        if(res >= steps) {
//            System.out.println("Warning: res = " + res + " > " + (steps - 1) + " low:" + low + "  high: " + high + " val: " + val + " bits: " + bits);
            res = steps - 1;
            rangeErrorsHigh = true;
        }
        return res;
    }

    public double decompressCoord(int val, double low, double high, int bits) {
        double res;
        int steps = (int)Math.pow(2, bits);
        res = low + (double)val * (high - low) / (double)(steps - 1);
        return res;
    }
*/

    /// angle (-1,1)  len (-1..1)
/*
    public int compressCoord11(double val, double min, double max, int bits) {
        return
        int bitsAngle = bits / 2;
        int bitsLen = bits - bitsAngle;
        int codeAngle = this.compressCoord(angle, -1.0, 1.0, bitsAngle);
        int codeLen = this.compressCoord(len, -max, max, bitsLen);
        return (codeAngle << bitsLen) + codeLen;
    }

    public double[] decompressCoordRad(int code, double max, int bits) {
        int bitsAngle = bits / 2;
        int bitsLen = bits - bitsAngle;
        int codeAngle = code >> bitsLen;
        int codeLen = code - (codeAngle << bitsLen);

        double result[] = {this.decompressCoord(codeAngle, -1.0, 1.0, bitsAngle) ,
                           this.decompressCoord(codeLen, -max, max, bitsLen)};
        return result;
    }
 *
 */

//    double expon = 3.14159;
//    double expon = 1.44;
    double expon = Math.E;
    public int compressCoord(double val, double low, double high, int bits) {
        if(val < low) {
            rangeErrorsLow = true;
            val = low;
//            System.out.println("Warning: low:" + low + "  high: " + high + " val: " + val + " bits: " + bits);
        }
        if(val > high) {
            rangeErrorsHigh = true;
            val = high;
//            System.out.println("Warning: low:" + low + "  high: " + high + " val: " + val + " bits: " + bits);
        }

        int res;
        int steps = (int)Math.pow(2, bits);
        int stepsHalf = steps / 2;
        double mid = (low + high) * 0.5;
        mid = 0;

        if(val < mid) {
            double y = (val - low) / (mid - low);
            res = (int)Math.round((1.0 - Math.sqrt(1.0 - y * y)) * (stepsHalf));
        } else {
            double y = (val - mid) / (high - mid);
            res = stepsHalf + (int)Math.round(Math.sqrt(1.0 - (1.0 - y) * (1.0 - y)) * (stepsHalf - 1));
        }

        if(val < mid) {
            double y = (val - low) / (mid - low);
            res = (int)Math.round((1.0 - Math.pow(1-y, 1.0/expon)) * (stepsHalf));
        } else {
            double y = (val - mid) / (high - mid);
            res = stepsHalf + (int)Math.round(Math.pow(y, 1.0/expon) * (stepsHalf - 1));
        }
/**/

        if((res < 0) || (val < low)) {
            rangeErrorsLow = true;
            res = 0;
//            System.out.println("Warning: res = " + res + " < " + " 0 " + " low:" + low + "  high: " + high + " val: " + val + " bits: " + bits);
        }
        if((res >= steps) || (val > high)) {
            rangeErrorsHigh = true;
            res = steps - 1;
//            System.out.println("Warning: res = " + res + " > " + (steps - 1) + " low:" + low + "  high: " + high + " val: " + val + " bits: " + bits);
        }
        return res;
    }

    public double decompressCoord(int val, double low, double high, int bits) {
        double res;
        int steps = (int)Math.pow(2, bits);
        int stepsHalf = steps / 2;
        double mid = (low + high) * 0.5;
        mid = 0;

        if(val < stepsHalf) {
            double x = (double)val / (double) stepsHalf;
            res = low + Math.sqrt(1.0 - (1.0 - x) * (1.0 - x)) * (mid - low);
        } else {
            double x = (double)(val - stepsHalf) / (double) (stepsHalf - 1);
            res = mid + (1.0 - Math.sqrt(1.0 - x * x)) * (high - mid);
        }

        if(val < stepsHalf) {
            double x = (double)val / (double) stepsHalf;
            res = low + (1.0 - Math.pow(1.0 - x, expon)) * (mid - low);
        } else {
            double x = (double)(val - stepsHalf) / (double) (stepsHalf - 1);
            res = mid + (Math.pow(x, expon)) * (high - mid);
        }
/**/
        return res;
    }


    public static class HuffEntry {
        int     cnt;
        int     value;
    }

    public void addBezCurve(Graphics g, double[] vcontrolPoints) {
        int segs = 30;
        double step = (1.0 / (double)segs);
        for(int i = 0; i < segs; i++) {
            double t = (double)i * step;
            double t2 = (double)(i+1) * step;
            double[] vals1 = this.evalBezier(vcontrolPoints, t);
            double[] vals2 = this.evalBezier(vcontrolPoints, t2);
            if(g != null)
                g.drawLine((int)(vals1[0] * (double)sizeX), (int)(vals1[1] * (double)sizeY), (int)(vals2[0] * (double)sizeX), (int)(vals2[1] * (double)sizeY));
        }
    }

    public void DrawBezPath(Graphics g, BezPath path) {
        g.setColor(Color.black);
        if(path.segments.size() == 0)
            return;
        BezSegment last = null;
        Iterator<BezSegment> iter = path.segments.iterator();
        while(iter.hasNext()) {
            BezSegment seg = iter.next();
            double[] cp = {seg.p0[0], seg.p0[1],
                           seg.p1[0], seg.p1[1],
                           seg.p2[0], seg.p2[1],
                           seg.p3[0], seg.p3[1]};
            addBezCurve(g, cp);
            last = seg;
        }
        BezSegment first = path.segments.firstElement();
    }

    static class StoreEntry {
        boolean isCoord = true;
        Integer coord;
        int startOffset;
        int len;

        public StoreEntry(int val) {
            this.isCoord = true;
            this.coord = val;
        }

        public StoreEntry(int offset, int len) {
            this.isCoord = false;
            this.startOffset = offset;
            this.len = len;
        }
    }

    LinkedList<BezPath> lastPaths = new LinkedList<BezPath>();

    BezPath GetFromLastPathes(int pnr, int segnr, int len) {
        BezPath result = new BezPath();
        BezPath path = lastPaths.get(pnr);
        for(int i = 0; i < len; i++) {
            BezSegment seg = path.segments.get((segnr - i + path.segments.size()) % path.segments.size());
            double[] cp = {seg.p3[0],seg.p3[1],
                           seg.p2[0],seg.p2[1],
                           seg.p1[0],seg.p1[1],
                           seg.p0[0],seg.p0[1]};
            double[] vcp = {seg.v3[0],seg.v3[1],
                           seg.v2[0],seg.v2[1],
                           seg.v1[0],seg.v1[1],
                           seg.v0[0],seg.v0[1]};
            BezSegment nseg = new BezSegment(cp,vcp);
            result.segments.add(nseg);
        }
        return result;
    }

    double[] lastVCP = null;
    BezSegment lastBezSeg = null;
    public void addBezCurveStub(Graphics g,double[] vcp, BezSegment bezSeg) {
        if(lastVCP != null)
            addBezCurve(g,lastVCP);
        lastVCP = vcp;
        lastBezSeg = bezSeg;
    }


    int debCacheCount = 0;
    public double CompressPathNew(Graphics g, BezPath path, BitWriter bw, double lenSize) {
        double resultMaxErrorSqr = 0;
        if(path.segments.size() == 0)
            return resultMaxErrorSqr;

        if(g != null)
            System.out.println("cachedeb: new contour");

        // lookup and start with longest cache entry
        int bestPos = 0;
        int bestMatch = 0;
        for(int i = 0; i < path.segments.size(); i++) {
            path.Lookup(0, lastPaths);
            if(path.matchLen > bestMatch) {
                bestMatch = path.matchLen;
                bestPos = i;
            }
            path.segments.add(path.segments.remove(0));
        }
        System.out.println("Shifting " + bestPos + " to get longest " + bestMatch);
        for(int i = 0; i < bestPos; i++)
            path.segments.add(path.segments.remove(0));

        BezSegment first = path.segments.firstElement();
        BezSegment last = path.segments.lastElement();
        double[] firstSegStartPos = null;

//        LinkedList<StoreEntry> coords = new LinkedList<StoreEntry>();
        double[] pen = {first.p0[0],first.p0[1]};
        double[] vpen = {0,0};
        double[] lastCtl2 = {0,0};

        bw.WriteUnsignedInt("CMD", ESVG.COMMAND_CONTOUR, ESVG.COMMAND_BITS);
        bw.WriteUnsignedInt("PATHANGLEBITS", Config.PATH_COORD_ANGLE_BITS - Config.MIN_PATH_BITS, Config.PATH_BITS_BITS);
        bw.WriteUnsignedInt("PATHLENBITS", Config.PATH_COORD_LEN_BITS - Config.MIN_PATH_BITS, Config.PATH_BITS_BITS);
        if(g != null)
            System.out.println("dbxmas  writing bits angle=" + Config.PATH_COORD_ANGLE_BITS + " len=" + Config.PATH_COORD_LEN_BITS);

        int plenSize = (int)(lenSize * Math.pow(2, Config.MIN_MAX_BITS - 1));
        bw.WriteSignedInt("MOVEOFFRANGE", (int)plenSize, Config.MIN_MAX_BITS);
        lenSize = (double)plenSize / Math.pow(2, Config.MIN_MAX_BITS - 1);
/*
        for(int i = 0; i < 2; i++) {
            int prec = 1 << (Config.MOVE_COORD_BITS - 1);
            int c = (int)(pen[i] * (double)prec);
            bw.WriteSignedInt("INITMOVE", c, Config.MOVE_COORD_BITS);
            vpen[i] = (double)c / (double)prec;
        }

        lastCtl2[0] = vpen[0];
        lastCtl2[1] = vpen[1];
*/
        System.out.println("------- New Path ------ " + "segs=" + path.segments.size() + "  oldPaths=" + lastPaths.size());
        int pos = -1;
        double lastLen          = 0;

        boolean lastUsed = false;

        if(g != null) {
            System.out.println("PATH NC: bits = " + Config.PATH_COORD_LEN_BITS);
            System.out.println("DBXMAS bitpos " + bw.bits.size());
        }
        Iterator<BezSegment> iter = path.segments.iterator();
        while(iter.hasNext()) {
            BezSegment seg = iter.next();
            double[] cp = {seg.p0[0], seg.p0[1],
                           seg.p1[0], seg.p1[1],
                           seg.p2[0], seg.p2[1],
                           seg.p3[0], seg.p3[1]};

            pos++;
            path.Lookup(pos, lastPaths);

            if((Config.USECACHE) && (path.matchLen > 0)) {
                for(int i = 0; i < path.matchLen - 1; i++)
                    seg = iter.next();

                boolean adjustLastSegment = true;

                BezPath cachedPath = this.GetFromLastPathes(path.matchPathNr, path.matchPathSegNr, path.matchLen);
                Iterator<BezSegment> it = cachedPath.segments.iterator();
                while(it.hasNext()) {
                    BezSegment s = it.next();
                    double[] vcp = {s.v0[0],s.v0[1],
                                   s.v1[0],s.v1[1],
                                   s.v2[0],s.v2[1],
                                   s.v3[0],s.v3[1]};
                    if(firstSegStartPos == null) {
                        firstSegStartPos = new double[2];
                        firstSegStartPos[0] = vcp[0];
                        firstSegStartPos[1] = vcp[1];
                    }

                    // if last segment in path is reached, make sure that endpoint equals start point
                    if((seg == last) && (!it.hasNext())) {
                        vcp[6] = firstSegStartPos[0];
                        vcp[7] = firstSegStartPos[1];
                        lastUsed = true;
                    }
                    if(adjustLastSegment) {
                        adjustLastSegment = false;
                        if(lastVCP != null) {
                            lastVCP[6] = vcp[0];
                            lastVCP[7] = vcp[1];
                            if(lastBezSeg != null) {
                                lastBezSeg.v3[0] = vcp[0];
                                lastBezSeg.v3[1] = vcp[1];
                            }
                        }
                    }

                    addBezCurveStub(g,vcp, null);
                    if(g != null)
                        System.out.println("CACHEDEB " + (debCacheCount++) + ":" + vcp[0] + " : " + vcp[1] + " : " + vcp[2] + " : " + vcp[3] + " : " + vcp[4] + " : " + vcp[5] + " : " + vcp[6] + " : " + vcp[7]);

                    lastCtl2[0] = vcp[4];
                    lastCtl2[1] = vcp[5];
                    vpen[0] = vcp[6];
                    vpen[1] = vcp[7];
                }

                if(g != null)
                    System.out.println("Writing cache: idx=" + path.matchIndex + " len=" + path.matchLen);
                bw.WriteUnsignedInt("CACHEMARKER", 0, 1);
                bw.WriteUnsignedInt("CACHED INDEX", path.matchIndex, Config.CACHE_IDX_BITS);
                bw.WriteUnsignedInt("CACHED LEN", path.matchLen, Config.CACHE_LEN_BITS);
                if(g != null)
                    System.out.println("DBXMAS cache idx " + path.matchIndex + " len " + path.matchLen);
//                coords.add(new StoreEntry(path.matchIndex, path.matchLen));

                pen[0] = seg.p3[0];
                pen[1] = seg.p3[1];
                pos += path.matchLen - 1;
                continue;
            }
            bw.WriteUnsignedInt("COORDMARKER", 1, 1);

            if(pos == 0) {
                // write initial pen position
                for(int i = 0; i < 2; i++) {
                    int prec = 1 << (Config.MOVE_COORD_BITS - 1);
                    int c = (int)(pen[i] * (double)prec);
                    bw.WriteSignedInt("INITMOVE", c, Config.MOVE_COORD_BITS);
                    vpen[i] = (double)c / (double)prec;
                }

                lastCtl2[0] = vpen[0];
                lastCtl2[1] = vpen[1];
            }

            if((pen[0] != seg.p0[0]) || (pen[1] != seg.p0[1])) {
                System.out.println("Error: Pen is different : " + pen[0] + " != " + seg.p0[0] + "  " + pen[1] + " != " + seg.p0[1]);
//                System.exit(0);
            }

            boolean predictNextPos = true;

            // save 3 coordinates
            double controlPoints[] = { seg.p0[0],seg.p0[1],
                                       seg.p1[0],seg.p1[1],
                                       seg.p2[0],seg.p2[1],
                                       seg.p3[0],seg.p3[1] };
            double vcontrolPoints[] = { vpen[0], vpen[1],
                                        vpen[0], vpen[1],
                                        0,0,
                                        0,0};

            if(predictNextPos) {
                vcontrolPoints[2] = Clamp(0, vpen[0] + (vpen[0] - lastCtl2[0]), 1);
                vcontrolPoints[3] = Clamp(0, vpen[1] + (vpen[1] - lastCtl2[1]), 1);
            }
            
//            System.out.println("Writing raw");
            for(int i = 1; i < 4; i++) {
                double x = controlPoints[i * 2] - vcontrolPoints[i * 2];
                double y = controlPoints[i * 2 + 1] - vcontrolPoints[i*2 + 1];
                double maxLen = Math.sqrt(2);
                double EPSILON = 0.00000001;
                double angle = (Math.atan2(y, x) / (2.0 * Math.PI + EPSILON)) + 0.5;
                double len = Math.sqrt(x*x + y*y) / maxLen;
                double diffLen = len - lastLen;
                    
                int anglePrec = (1 << (Config.PATH_COORD_ANGLE_BITS));
                int angleCode = (int)(angle * (double)anglePrec);
//                int angleCode = this.compressCoord(diffAngle, -1.0, 1.0, Config.PATH_ANGLE_BITS);
                int lenCode = this.compressCoord(diffLen, -lenSize, lenSize, Config.PATH_COORD_LEN_BITS);
                bw.WriteUnsignedInt("PATHCOORDANGLE", angleCode, Config.PATH_COORD_ANGLE_BITS);
                bw.WriteUnsignedInt("PATHCOORDLEN", lenCode, Config.PATH_COORD_LEN_BITS);
                if(g != null)
                    System.out.println("blub");
                double vdiff[] = { 0,
                                   this.decompressCoord(lenCode, -lenSize, lenSize, Config.PATH_COORD_LEN_BITS)};
                double vres[] = { Clamp(0, (double)angleCode / (double)anglePrec, 1),
                                  Clamp(0, lastLen + vdiff[1], 1) };
                if(g != null)
                    System.out.println("DBXMAS LenCode "+ lenCode + " lenSize " + lenSize + " bits " + Config.PATH_COORD_LEN_BITS + " lastLen " + lastLen );
                lastLen = vres[1];
                
                double vangle = (vres[0] - 0.5) * (2.0 * Math.PI + EPSILON);
                double vlen = vres[1] * maxLen;

                vcontrolPoints[i * 2] += Math.cos(vangle) * vlen;
                vcontrolPoints[i * 2+1] += Math.sin(vangle) * vlen;

//                System.out.println("Point: " + vcontrolPoints[i * 2] + ", " + vcontrolPoints[i * 2 + 1]);

                // predict next
                if(i < 3) {
                    vcontrolPoints[(i+1) * 2]     = Clamp(0, vcontrolPoints[i * 2], 1);
                    vcontrolPoints[(i+1) * 2 + 1] = Clamp(0, vcontrolPoints[i * 2 + 1], 1);

                    if(predictNextPos) {
                        vcontrolPoints[(i + 1)*2]   = Clamp(0, vcontrolPoints[i*2]   + (vcontrolPoints[i*2]   - vcontrolPoints[(i - 1)*2]), 1);
                        vcontrolPoints[(i + 1)*2+1] = Clamp(0, vcontrolPoints[i*2+1] + (vcontrolPoints[i*2+1] - vcontrolPoints[(i - 1)*2+1]), 1);
                    }
                }
            }

            if(firstSegStartPos == null) {
                firstSegStartPos = new double[2];
                firstSegStartPos[0] = vcontrolPoints[0];
                firstSegStartPos[1] = vcontrolPoints[1];
            }


            // if last segment in path is reached, make sure that endpoint equals start point
            if(seg == last) {
                vcontrolPoints[6] = firstSegStartPos[0];
                vcontrolPoints[7] = firstSegStartPos[1];
                lastUsed = true;
            }

            if(g != null)
                System.out.println("CACHEDEB NORMAL :" + vcontrolPoints[0] + " : " + vcontrolPoints[1] + " : " + vcontrolPoints[2] + " : " + vcontrolPoints[3] + " : " + vcontrolPoints[4] + " : " + vcontrolPoints[5] + " : " + vcontrolPoints[6] + " : " + vcontrolPoints[7]);

            seg.v0[0] = vcontrolPoints[0];
            seg.v0[1] = vcontrolPoints[1];
            seg.v1[0] = vcontrolPoints[2];
            seg.v1[1] = vcontrolPoints[3];
            seg.v2[0] = vcontrolPoints[4];
            seg.v2[1] = vcontrolPoints[5];
            seg.v3[0] = vcontrolPoints[6];
            seg.v3[1] = vcontrolPoints[7];

            addBezCurveStub(g, vcontrolPoints, seg);

            lastCtl2[0] = vcontrolPoints[4];
            lastCtl2[1] = vcontrolPoints[5];
            vpen[0] = vcontrolPoints[6];
            vpen[1] = vcontrolPoints[7];
            pen[0] = controlPoints[6];
            pen[1] = controlPoints[7];

            // calculate error
            for(int i = 0; i < 4; i++) {
                double err = ( (vcontrolPoints[i * 2] - controlPoints[i * 2]) * (vcontrolPoints[i * 2] - controlPoints[i * 2]) +
                         (vcontrolPoints[i * 2+1] - controlPoints[i * 2+1]) * (vcontrolPoints[i * 2+1] - controlPoints[i * 2+1]));
                err = err / (lenSize * lenSize);
                if(err > resultMaxErrorSqr)
                    resultMaxErrorSqr = err;
            }
        }
        addBezCurveStub(g,null,null);
        bw.WriteUnsignedInt("CACHEMARKER", 0, 1);
        bw.WriteUnsignedInt("CACHED INDEX", 0, Config.CACHE_IDX_BITS);
        bw.WriteUnsignedInt("CACHED LEN", 0, Config.CACHE_LEN_BITS);
        if(g != null)
            System.out.println("DBXMAS end cache idx 0 len 0");
        return resultMaxErrorSqr;
    }

    double perrAvg = 0;
    double perrSqr = 0;
    double pmaxErrSqr = 0;
    int pcnt = 0;
    int segmentCnt = 0;
//    LinkedList<Integer> pathCoords = new LinkedList<Integer>();
    double[] vpen = new double[2];
    double[] vsubPathStart = null;

    public static double Clamp(double min, double val, double max) {
        if(val < min)
            return min;
        if(val > max)
            return max;
        return val;
    }

    public static double Wrap(double min, double val, double max) {
        double len = max - min;
        while(val < min)
            val += len;
        while(val > max)
            val -= len;
        return val;
    }

    public Collection<BezPath> compressPath(Graphics g, Path path, double min, double max) {
            LinkedList<BezPath> bezPathesResult = new LinkedList<BezPath>();
            BezPath bezPath = new BezPath();
            double avgVal = 0;
            double avgValSqr = 0;
            int numVal = 0;

            LinkedList<Double[]> realCoords = new LinkedList<Double[]>();
            LinkedList<Double[]> vCoords = new LinkedList<Double[]>();

            int pos = 0;
            double[] pen = new double[2];
            vpen = new double[2];
            double[] subPathStart = null;
            vsubPathStart = null;
            double[] lastControlPoint2 = null;
            double[] vlastControlPoint2 = null;
            double[] controlPoints = new double[4 * 2];
            double[] vcontrolPoints = new double[4 * 2];
            while(pos < path.commands.length) {
                String cmd = path.commands[pos++];
                if(cmd.equals("m") || cmd.equals("M")) {
                    while(!path.commands[pos].matches("[a-zA-Z]")) {
                        if(segmentCnt != 0) {
                            segmentCnt = 0;
                            bezPathesResult.add(bezPath);
                            bezPath = new BezPath();
                        }
                        if(cmd.equals("m")) {
                            pen[0] += Double.parseDouble(path.commands[pos++]);
                            pen[1] += Double.parseDouble(path.commands[pos++]);
                        } else {
                            pen[0] = Double.parseDouble(path.commands[pos++]);
                            pen[1] = Double.parseDouble(path.commands[pos++]);
                        }
                    }
                } else if(cmd.equals("c") || cmd.equals("s")) {
                    if(segmentCnt == 0) {
                        bw.WriteUnsignedInt("CMD", ESVG.COMMAND_CONTOUR, ESVG.COMMAND_BITS);
                        vlastControlPoint2 = null;
                        lastControlPoint2 = null;
                        subPathStart = pen.clone();
                        for(int i = 0; i < 2; i++) {
                            int prec = 1 << (Config.MOVE_COORD_BITS - 1);
                            int c = (int)(pen[i] * (double)prec);
//                            bw.WriteSignedInt("MOVE", c, Config.MOVE_COORD_BITS);
                            vpen[i] = (double)c / (double)prec;
                        }

                        segmentCnt = 0;

                        for(int d = 0; d < 2; d++) {
                            double val = pen[d] - vpen[d];
                            if(val < testMin)   testMin = val;
                            if(val > testMax)   testMax = val;
                            vpen[d] += val;
                        }
                        vsubPathStart = vpen.clone();
                    }

                    while(!path.commands[pos].matches("[a-zA-Z]")) {
                        segmentCnt++;
                        controlPoints[0] = pen[0];
                        controlPoints[1] = pen[1];
                        int start = 1;
                        if(cmd.equals("s")) {
                            start = 2;
                            controlPoints[2] = pen[0] + (pen[0] - lastControlPoint2[0]);
                            controlPoints[3] = pen[1] + (pen[1] - lastControlPoint2[1]);
                        }
                        for(int i = start; i < 4; i++)
                            for(int d = 0; d < 2; d++) {
                                double v = Double.parseDouble(path.commands[pos++]);
                                this.testMax(d, v);
                                controlPoints[i * 2 + d] = pen[d] + v;
                            }
                        vcontrolPoints[0] = vpen[0];
                        vcontrolPoints[1] = vpen[1];

                        // do prediction
                        if(vlastControlPoint2 != null) {
                            vcontrolPoints[2] = vpen[0] + (vpen[0] - vlastControlPoint2[0]);
                            vcontrolPoints[3] = vpen[1] + (vpen[1] - vlastControlPoint2[1]);
                        } else {
                            vcontrolPoints[2] = vpen[0];
                            vcontrolPoints[3] = vpen[1];
                        }

                        for(int i = 1; i < 4; i++)
                            for(int d = 0; d < 2; d++) {
//                                double val = controlPoints[i * 2 + d] - vpen[d];
                                double val = controlPoints[i * 2 + d] - vcontrolPoints[i * 2 + d];
                                if(val < testMin)
                                    testMin = val;
                                if(val > testMax)
                                    testMax = val;
                                double vall;
                                if(val < 0)
                                    vall = val/(min);
                                else
                                    vall = val/(max);
                                avgVal += vall;
                                avgValSqr = vall*vall;
                                numVal++;
//                                vcontrolPoints[i * 2 + d] = vpen[d] + dval;
                                vcontrolPoints[i * 2 + d] = vcontrolPoints[i * 2 + d] + val;

                                // predict next
                                if(i != 3)
                                    vcontrolPoints[(i+1) * 2 + d] = vcontrolPoints[i * 2 + d] + (vcontrolPoints[i * 2 + d] - vcontrolPoints[(i-1) * 2 + d]);
                            }
                        for(int i = 0; i < 4; i++) {
                            Double real[] = new Double[2];
                            Double virt[] = new Double[2];
                            for(int d = 0; d < 2; d++) {
                                real[d] = controlPoints[i*2 + d];
                                virt[d] = vcontrolPoints[i*2 + d];
                            }
                            realCoords.add(real);
                            vCoords.add(virt);
                        }
                        for(int i = 0; i < 4; i++) {
                            for(int d = 0; d < 2; d++) {
                                double err = vcontrolPoints[i * 2 + d] - controlPoints[i * 2 + d];
                                perrSqr += err*err;
                                perrAvg += Math.abs(err);
                                pcnt++;

                                if(err*err > pmaxErrSqr)
                                    pmaxErrSqr = err*err;
                            }
                        }
/*
                        if(g != null)
                            this.addBezCurve(g, vcontrolPoints);
/**/
                        BezSegment bezSegment = new BezSegment(controlPoints, vcontrolPoints);
                        bezPath.segments.add(bezSegment);

                        lastControlPoint2 = new double[2];
                        lastControlPoint2[0] = controlPoints[4];
                        lastControlPoint2[1] = controlPoints[5];
                        vlastControlPoint2 = new double[2];
                        vlastControlPoint2[0] = vcontrolPoints[4];
                        vlastControlPoint2[1] = vcontrolPoints[5];
                        pen[0] = controlPoints[6];
                        pen[1] = controlPoints[7];
                        vpen[0] = vcontrolPoints[6];
                        vpen[1] = vcontrolPoints[7];
                    }
                } else if(cmd.equals("h")) {
                    
/*
                    System.out.println("Error: h not supported");
                    System.exit(0);
 */
                    while(!path.commands[pos].matches("[a-zA-Z]")) {
                        double dx = Double.parseDouble(path.commands[pos++]);
                        controlPoints[0] = pen[0];
                        controlPoints[1] = pen[1];
                        controlPoints[2] = pen[0] + dx * 0.5;
                        controlPoints[3] = pen[1];
                        controlPoints[4] = pen[0] + dx * 0.5;
                        controlPoints[5] = pen[1];
                        controlPoints[6] = pen[0] + dx;
                        controlPoints[7] = pen[1];
                        BezSegment bezSegment = new BezSegment(controlPoints, controlPoints);
                        bezPath.segments.add(bezSegment);
                        pen[0] = pen[0] + dx;
                    }
                } else if(cmd.equals("v")) {
                    while(!path.commands[pos].matches("[a-zA-Z]")) {
                        double dy = Double.parseDouble(path.commands[pos++]);
                        controlPoints[0] = pen[0];
                        controlPoints[1] = pen[1];
                        controlPoints[2] = pen[0];
                        controlPoints[3] = pen[1] + dy * 0.5;
                        controlPoints[4] = pen[0];
                        controlPoints[5] = pen[1] + dy * 0.5;
                        controlPoints[6] = pen[0];
                        controlPoints[7] = pen[1] + dy;
                        BezSegment bezSegment = new BezSegment(controlPoints, controlPoints);
                        bezPath.segments.add(bezSegment);
                        pen[1] = pen[1] + dy;
                    }
                } else if(cmd.equals("l")) {
                    while(!path.commands[pos].matches("[a-zA-Z]")) {
                        double dx = Double.parseDouble(path.commands[pos++]);
                        double dy = Double.parseDouble(path.commands[pos++]);
                        controlPoints[0] = pen[0];
                        controlPoints[1] = pen[1];
                        controlPoints[2] = pen[0] + dx * 0.5;
                        controlPoints[3] = pen[1] + dy * 0.5;
                        controlPoints[4] = pen[0] + dx * 0.5;
                        controlPoints[5] = pen[1] + dy * 0.5;
                        controlPoints[6] = pen[0] + dx;
                        controlPoints[7] = pen[1] + dy;
                        BezSegment bezSegment = new BezSegment(controlPoints, controlPoints);
                        bezPath.segments.add(bezSegment);
                        pen[0] = pen[0] + dx;
                        pen[1] = pen[1] + dy;
                    }
                } else if(cmd.equalsIgnoreCase("z")) {
                    // do nothing
                    if(segmentCnt != 0) {
                        segmentCnt = 0;
                        bezPathesResult.add(bezPath);
                        bezPath = new BezPath();
                    }
                } else {
                    System.out.println("Unknown command [" + cmd + "]");
                }

            }
            if(segmentCnt != 0) {
                segmentCnt = 0;
                bezPathesResult.add(bezPath);
            }
            if(g != null) {
                System.out.println("Avg val: " + (avgVal / (double)numVal) + " squared: " + (avgValSqr / (double)numVal) + " min: " + min + " max: " + max + "  cnt: " + numVal);
            }

            // calc error
            perrAvg = 0;
            perrSqr = 0;
            pcnt = 0;
            pmaxErrSqr = 0;

            double bbmin[] = {Double.MAX_VALUE, Double.MAX_VALUE};
            double bbmax[] = {-Double.MAX_VALUE, -Double.MAX_VALUE};
            for(int i = 0; i < realCoords.size(); i++) {
                Double[] v = realCoords.get(i);
                if(v[0] < bbmin[0]) bbmin[0] = v[0];
                if(v[0] > bbmax[0]) bbmax[0] = v[0];
                if(v[1] < bbmin[1]) bbmin[1] = v[1];
                if(v[1] > bbmax[1]) bbmax[1] = v[1];
                v = vCoords.get(i);
                if(v[0] < bbmin[0]) bbmin[0] = v[0];
                if(v[0] > bbmax[0]) bbmax[0] = v[0];
                if(v[1] < bbmin[1]) bbmin[1] = v[1];
                if(v[1] > bbmax[1]) bbmax[1] = v[1];
            }
            double rad = Math.sqrt((bbmax[0] - bbmin[0]) * (bbmax[0] - bbmin[0]) + (bbmax[1] - bbmin[1]) * (bbmax[1] - bbmin[1]));
            for(int i = 0; i < realCoords.size(); i++) {
                Double[] r = realCoords.get(i);
                Double[] v = vCoords.get(i);
                double dist = Math.sqrt((v[0] - r[0]) * (v[0] - r[0]) + (v[1] - r[1]) * (v[1] - r[1]));
                double err = dist / rad;
                perrSqr += err*err;
                perrAvg += Math.abs(err);
                pcnt++;

                if(err*err > pmaxErrSqr)
                    pmaxErrSqr = err*err;

            }
/* */
            return bezPathesResult;
    }

    double testMin, testMax;
    boolean rangeErrorsLow;
    boolean rangeErrorsHigh;
    double tmin, tmax;
    public void compressPathStub(Graphics g, Path path) {
        tmin = -1;
        tmax = 1;
        double step = 1;
        BitWriter realBW = bw;
        testMin = Double.MAX_VALUE;
        testMax = -Double.MAX_VALUE;
        int it = 0;
        while(true) {
            bw = new BitWriter();
            rangeErrorsLow = false;
            rangeErrorsHigh = false;
            compressPath(g,path, tmin, tmax);
            if(rangeErrorsLow) {
                tmin -= step;
            } else if(rangeErrorsHigh) {
                tmax += step;
            } else
                break;
        }
        System.out.println("Best range: " + tmin + " - " + tmax + " iteration " + it++);
        bw = realBW;
        perrAvg = 0;
        perrSqr = 0;
        pcnt = 0;
        pmaxErrSqr = 0;
        compressPath(g,path, tmin, tmax);
    }

    static Double[] convertCoord(Double[] c) {
        Double[] res = new Double[2];
        res[0] = (c[0] - docMinX) * SVGDrawer.docInvSizeX;
        res[1] = (c[1] - docMinY) * SVGDrawer.docInvSizeY;
        return res;
    }

    BitWriter bw;
    HashMap<String, HuffEntry> umap = new HashMap<String, HuffEntry>();
    LinkedList<BezPath> bezPathes = new LinkedList<BezPath>();

    public int pub_pnr = 0;
    @Override
    public void paint(Graphics graphics) {
        if(bw != null)
            return;
        bw = new BitWriter();
        Graphics2D g = (Graphics2D) graphics;

        int uniqueCoords = 0;
        int cntCoords = 0;

        int minPathLenBits = Integer.MAX_VALUE;
        int maxPathLenBits = 0;
        int minPathAngleBits = Integer.MAX_VALUE;
        int maxPathAngleBits = 0;

        // write meta data
        bw.WriteUnsignedInt("CONSTS", Config.MOVE_COORD_BITS, 6);
        bw.WriteUnsignedInt("CONSTS", Config.PATH_SEGMENTCNT_BITS, 4);
        bw.WriteUnsignedInt("CONSTS", Config.MIN_MAX_BITS, 6);
        bw.WriteUnsignedInt("CONSTS", Config.MIN_PATH_BITS, 4);
        bw.WriteUnsignedInt("CONSTS", Config.PATH_BITS_BITS, 4);
        bw.WriteUnsignedInt("CONSTS", Config.CACHE_IDX_BITS, 4);
        bw.WriteUnsignedInt("CONSTS", Config.CACHE_LEN_BITS, 4);

        bw.WriteUnsignedInt("CONSTS", this.paths.size(), 16);
        int moveCoords = 0;

//        int minPathBits = Integer.MAX_VALUE;
//        int maxPathBits = 0;
        int numContours = 0;
        double minLow = Double.MAX_VALUE;
        double maxHigh = -Double.MAX_VALUE;
        double errAvg = 0;
        double errSqr = 0;
        int ccnt = 0;
        int pnr = -1;
        Iterator<Path> piter = this.paths.iterator();
        while(piter.hasNext()) {
            pnr++;
//            if(pnr == 6)  break;
            pub_pnr = pnr;
            Path path = piter.next();

            System.out.println("Col : " + path.color[0] + " " + path.color[1] + " " + path.color[2]);
            g.setColor(new Color((int)path.color[0],
                                 (int)path.color[1],
                                 (int)path.color[2]));
            double min = Double.MAX_VALUE;
            double max = -Double.MAX_VALUE;
            int pos = 0;
            while(pos < path.commands.length) {
                String cmd = path.commands[pos++];
                if(cmd.equals("c") || cmd.equals("s")) {
                    while(!path.commands[pos].matches("[a-zA-Z]")) {
                        int start = 0;
                        if(cmd.equals("s"))
                            start = 2;
                        for(int i = start; i < 6; i++) {
                            double v = Double.parseDouble(path.commands[pos++]);
                            if(v > max)
                                max = v;
                            if(v < min)
                                min = v;
                        }
                    }
                }

            }

            if(min < minLow)
                minLow = min;
            if(max > maxHigh)
                maxHigh = max;

            for(int i = 0; i < 3; i++)
                bw.WriteUnsignedInt("COLOR", (int)path.color[i], 8);

            min -= 2;
            max += 2;
            BitWriter realBW = bw;
            Config.PATH_COORD_LEN_BITS = Config.MIN_PATH_BITS;

            System.out.println("Testing path " + pnr + " with " + Config.PATH_COORD_LEN_BITS + " bits");
            bw = new BitWriter();
            this.compressPathStub(null, path);

            System.out.println("Best bits: " + Config.PATH_COORD_LEN_BITS);

            bw = realBW;
            errAvg = perrAvg;
            errSqr = perrSqr;
            ccnt = pcnt;
 /**/
//            bw.WriteUnsignedInt("PATHBITS", Config.PATH_COORD_BITS - Config.MIN_PATH_BITS, Config.PATH_BITS_BITS);
//            System.out.println("Path " + pnr + " offset: " + bw.bits.size() + " bits: " + Config.PATH_COORD_BITS);

            int pmin = (int)(tmin * Math.pow(2, Config.MIN_MAX_BITS - 1));
            int pmax = (int)(tmax * Math.pow(2, Config.MIN_MAX_BITS - 1));
//            bw.WriteSignedInt("MINMAX", (int)pmin, Config.MIN_MAX_BITS);
//            bw.WriteSignedInt("MINMAX", (int)pmax, Config.MIN_MAX_BITS);
            min = (double)pmin / Math.pow(2, Config.MIN_MAX_BITS - 1);
            max = (double)pmax / Math.pow(2, Config.MIN_MAX_BITS - 1);

            bw = new BitWriter();
            Collection<BezPath> bp = this.compressPath(graphics, path, min, max);
            // convert bezpathes
            Iterator<BezPath> bpIter = bp.iterator();
            while(bpIter.hasNext()) {
                Iterator<BezSegment> bsIter = bpIter.next().segments.iterator();
                while(bsIter.hasNext()) {
                    BezSegment bs = bsIter.next();
                    bs.p0 = convertCoord(bs.p0);
                    bs.p1 = convertCoord(bs.p1);
                    bs.p2 = convertCoord(bs.p2);
                    bs.p3 = convertCoord(bs.p3);
                }
            }

            bezPathes.addAll(bp);
            bw = realBW;
            Iterator<BezPath> pit = bp.iterator();
            while(pit.hasNext()) {
                Config.PATH_COORD_ANGLE_BITS = Config.MAX_PATH_BITS;
                BezPath curPath = pit.next();
                double size = 0.001;
                double step = 0.001;
                do {
                    size += step;
                    rangeErrorsLow = false;
                    rangeErrorsHigh = false;
                    this.CompressPathNew(null, curPath, new BitWriter(), size);
                } while((rangeErrorsLow) || rangeErrorsHigh);
//                size += step;
//                size *= 2.0;
/*
                int psize = (int)(size * Math.pow(2, Config.MIN_MAX_BITS - 1));
                bw.WriteSignedInt("MOVEOFFRANGE", (int)psize, Config.MIN_MAX_BITS);
                size = (double)psize / Math.pow(2, Config.MIN_MAX_BITS - 1);
*/


//                this.CompressPathNew(graphics, pit.next(), bw, min, max);
                double lastEsqr;
                double esqr = Double.MAX_VALUE;

                // try to find best bit count for lengthes
                Config.PATH_COORD_LEN_BITS = Config.MIN_PATH_BITS - 1;
                do {
                    lastEsqr = esqr;
                    Config.PATH_COORD_LEN_BITS++;
                    esqr = this.CompressPathNew(null, curPath, new BitWriter(), size);
                    System.out.println("*** ErrorSqr = " + esqr + " bits=" + Config.PATH_COORD_LEN_BITS + "   RangeErrors:" + (rangeErrorsLow || rangeErrorsHigh));

//                } while((esqr > Config.MAX_ERROR) && (esqr < lastEsqr));
                } while((esqr > Config.MAX_ERROR) && (Config.PATH_COORD_LEN_BITS < Config.MAX_PATH_BITS));
                boolean sucker = (esqr > Config.MAX_ERROR);

                // now try to find best bitcount for angles
                Config.PATH_COORD_ANGLE_BITS++;
                esqr = Double.MAX_VALUE;
                do {
                    lastEsqr = esqr;
                    Config.PATH_COORD_ANGLE_BITS--;
                    esqr = this.CompressPathNew(null, curPath, new BitWriter(), size);
                    System.out.println("*** ErrorSqr = " + esqr + " anglebits=" + Config.PATH_COORD_ANGLE_BITS + "   RangeErrors:" + (rangeErrorsLow || rangeErrorsHigh));
                } while((esqr <= Config.MAX_ERROR) && (Config.PATH_COORD_ANGLE_BITS > Config.MIN_PATH_BITS));
                Config.PATH_COORD_ANGLE_BITS++;

                if(sucker) {
                    // shitty error
                    Config.PATH_COORD_LEN_BITS = Config.AVG_PATH_BITS;
                    Config.PATH_COORD_ANGLE_BITS = Config.AVG_PATH_BITS;
                }

                this.CompressPathNew(graphics, curPath, bw, size);
                this.lastPaths.add(curPath);
                if(Config.PATH_COORD_LEN_BITS < minPathLenBits)
                    minPathLenBits = Config.PATH_COORD_LEN_BITS;
                if(Config.PATH_COORD_LEN_BITS > maxPathLenBits)
                    maxPathLenBits = Config.PATH_COORD_LEN_BITS;
                if(Config.PATH_COORD_ANGLE_BITS < minPathAngleBits)
                    minPathAngleBits = Config.PATH_COORD_ANGLE_BITS;
                if(Config.PATH_COORD_ANGLE_BITS > maxPathAngleBits)
                    maxPathAngleBits = Config.PATH_COORD_ANGLE_BITS;
            }
            numContours += bp.size();
//            this.DrawBezPath(graphics, bezPath);
//            this.compressPathStub(graphics, path);
            bw.WriteUnsignedInt("CMD", ESVG.COMMAND_PATHEND, ESVG.COMMAND_BITS);
        }
        System.out.println("Number of contours: " + numContours);
        System.out.println("Max : " + this.maxVals[0] + " " + this.maxVals[1]);
        System.out.println("Avg Err: " + (errAvg / (double)ccnt));
        System.out.println("Avg ErrSqr: " + (errSqr / (double)ccnt));
        System.out.println("MinLow: " + minLow + "  maxHigh: "+ maxHigh);
        System.out.println(paths.size() + " pathes");
        int minMaxBits = 2 * paths.size() * Config.MIN_MAX_BITS;
        System.out.println("MinMaxCosts: " + minMaxBits + " bits (" + (minMaxBits / 8) + " bytes)");
        int colorBits = paths.size() * 3 * 8;
        System.out.println("ColorCosts: " + colorBits + " bits (" + (colorBits / 8) + " bytes)");
        int moveBits = moveCoords * Config.MOVE_COORD_BITS;
        System.out.println("MoveCoords: " + moveCoords + " -> " + moveBits + " bits (" + (moveBits / 8) + " bytes)");
        int coordBits = cntCoords * Config.PATH_COORD_LEN_BITS;
        System.out.println("Unique Path coords: " + uniqueCoords + "/" + cntCoords + "   costs: " + coordBits + " bits (" + (coordBits / 8) + " bytes)");
        System.out.println("PathCoordLenBits: Min = " + minPathLenBits + "  max = " + maxPathLenBits);
        System.out.println("PathCoordAngleBits: Min = " + minPathAngleBits + "  max = " + maxPathAngleBits);
//        System.out.printl

        bw.PrintStats();
        System.out.println("Filesize: " + bw.bits.size() + " bits = " + (bw.bits.size() / 8) + " bytes");
        bw.WriteFile("output.esvg");
//        System.exit(0);
    }

}
