public class Bitmap {

    private static final Logger logger = Logger.getInstance();

    private final int width;
    private final int height;
    private final byte buffer[];

    public Bitmap(int width, int height) {
        this.width = width;
        this.height = height;
        this.buffer = new byte[width * height];
    }

    public Bitmap(int width, int height, byte fillerColorIndex) {
        this(width, height);
        fillWithColorIndexValue(fillerColorIndex);
    }

    public Bitmap(int width, int height, byte[] buffer) {
        this.width = width;
        this.height = height;
        this.buffer = buffer;
    }

    public Bitmap(Bitmap bitmap) {
        this.width = bitmap.width;
        this.height = bitmap.height;
        this.buffer = Arrays.copyOf(bitmap.buffer, bitmap.buffer.length);
    }

    private void fillWithColorIndexValue(byte colorIndex) {
        for (int i = 0; i < width * height; i++) {
            buffer[i] = colorIndex;
        }
    }

    public void fillRectangularWithColorIndex(int rectX, int rectY, int rectWidth, int rectHeight, byte colorIndex) {
        int xMax = rectX + rectWidth;
        if (xMax > width) {
            xMax = width;
        }

        int yMax = rectY+rectHeight;
        if (yMax > height) {
            yMax = height;
        }

        for (int y = rectY; y < yMax; y++) {
            int yOfs = y * width;
            for (int x = rectX; x < xMax; x++) {
                buffer[yOfs + x] = colorIndex;
            }
        }
    }

    public BufferedImage getImage(ColorModel colorModel) {
        DataBuffer dataBuffer = new DataBufferByte(buffer, width * height);
        SampleModel sampleModel = new SinglePixelPackedSampleModel(DataBuffer.TYPE_BYTE, width, height, new int[]{ 0xff });
        WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer, null);
        return new BufferedImage(colorModel, raster, false, null);
    }

    public int getPrimaryColorIndex(byte[] alpha, int alphaThreshold, byte[] luma) {
        int colorIndexCount = alpha.length;
        int[] histogram = createHistogram(colorIndexCount);
        weightHistogramWithAlphaAndLumaValues(histogram, alpha, alphaThreshold, luma);
        return getColorIndexWithTheHighestValue(histogram);
    }

    private int[] createHistogram(int colorIndexCount) {
        int histogram[] = new int[colorIndexCount];
        Arrays.fill(histogram, 0);
        for (byte b : buffer) {
            histogram[(b & 0xff)]++;
        }
        return histogram;
    }

    private void weightHistogramWithAlphaAndLumaValues(int[] histogram, byte[] alphaValues, int alphaThreshold, byte[] lumaValues) {
        for (int i = 0; i < histogram.length; i++) {
            int alpha = alphaValues[i] & 0xff;
            if (alpha < alphaThreshold) {
                alpha = 0;
            }
            histogram[i] = (histogram[i] * alpha + 128) / 256; // prefer opaque

            int luma = lumaValues[i] & 0xff;
            histogram[i] = (histogram[i] * luma + 128) / 256; // prefer light
        }
    }

    private int getColorIndexWithTheHighestValue(int[] histogram) {
        int maxValue = 0;
        int colorIndex = 0;
        for (int i=0; i < histogram.length; i++) {
            if (histogram[i] > maxValue) {
                maxValue = histogram[i];
                colorIndex = i;
            }
        }
        return colorIndex;
    }

    public int getHighestVisibleColorIndex(byte[] alphaValues) {
        int maxColorIndex = 0;
        for (byte b : buffer) {
            int colorIndex = b & 0xff;
            if ((alphaValues[colorIndex] & 0xff) > 0) {
                if (colorIndex > maxColorIndex) {
                    maxColorIndex = colorIndex;
                    if (maxColorIndex == 255) {
                        break;
                    }
                }
            }
        }
        return maxColorIndex;
    }

    /**
     * Convert a palettized Bitmap (where each palette entry has individual alpha)
     * to a Bitmap with N color palette, where:
     * index0 = transparent (background), index1 = light color (pattern), ... , indexN-2 = dark color, indexN-1 = black.
     *
     * For N-1 luminances, N-2 thresholds are needed
     *                  lumaThreshold[0] is the threshold for the lightest color (-> index 1)
     *                  lumaThreshold[N-2] is the threshold for the darkest color (-> index N-1)
     */
    public Bitmap getBitmapWithNormalizedPalette(byte[] alphaValues, int alphaThreshold, byte[] lumaValues, int lumaThreshold[]) {
        Bitmap bm = new Bitmap(width, height);
        HashMap<Integer, Integer> p = new HashMap<Integer, Integer>();

        int newColorIndex;
        for (int i = 0; i < buffer.length; i++) {
            int colorIndex = buffer[i] & 0xff;
            int alpha = alphaValues[colorIndex] & 0xff;
            int luma = lumaValues[colorIndex] & 0xff;

            Integer existingColorIndex = p.get((alpha << 8) | luma);

            if (existingColorIndex != null) {
                newColorIndex = existingColorIndex;
            }
            else {
                if (alpha < alphaThreshold) {
                    newColorIndex = 0; // transparent color
                } else {
                    newColorIndex = 1; // default: lightest color
                    for (int threshold : lumaThreshold) {
                        if (luma > threshold) {
                            break;
                        }
                        newColorIndex++; // try next darker color
                    }
                }
                p.put((alpha << 8) | luma, newColorIndex);
            }
            bm.buffer[i] = (byte)newColorIndex;
        }
        return bm;
    }

    /**
     * Scales a palettized Bitmap (where each palette entry has individual alpha) using bilinear filtering
     * to a Bitmap with N color palette, where:
     * index0 = transparent, index1 = light color, ... , indexN-2 = dark color, indexN-1 = black.
     * @param sizeX		Target width
     * @param sizeY		Target height
     * @param pal 		Palette of the source Bitmap
     * @param alphaThr	Threshold for alpha (transparency), lower = more transparent
     * @param lumThr    Threshold for luminances. For N-1 luminances, N-2 thresholds are needed
     *                  lumThr[0] is the threshold for the lightest color (-> idx 1)
     *                  lumThr[N-2] is the threshold for the darkest color (-> idx N-1)
     * @return Scaled Bitmap which uses a fixed frame Palette.
     */
    public Bitmap scaleBilinearLm(final int sizeX, final int sizeY, final Palette pal, final int alphaThr, final int lumThr[]) {
        final byte[] cy = pal.getY();
        final byte[] a = pal.getAlpha();

        final double scaleX = (double)(width - 1) / (sizeX - 1);
        final double scaleY = (double)(height - 1) / (sizeY - 1);

        int lastCY = 0;
        int lastA  = 0;
        int lastColIdx = 0; // 0 is the transparent color

        Bitmap trg = new Bitmap(sizeX, sizeY);

        for (int yt = 0; yt < sizeY; yt++) {
            double ys = yt * scaleY; // source coordinate
            int ysi = (int)ys;
            double wy = (ys - ysi);
            double wy1 = 1.0 - wy;

            for (int xt = 0; xt < sizeX; xt++) {
                double xs = xt * scaleX; // source coordinate
                int xsi = (int)xs;
                double wx = (xs - xsi); // weight factor

                double wx1 = 1.0 - wx;

                // interpolate pixel

                // top left
                double w = wx1 * wy1;
                int idx = getPixel(xsi, ysi) & 0xff;
                double at = (a[idx] & 0xff) * w;
                double cyt = (cy[idx] & 0xff) * w;

                // top right
                if (xsi < width - 1) {
                    w = wx * wy1;
                    idx = getPixel(xsi + 1, ysi) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    cyt += (cy[idx] & 0xff) * w;
                }  // else assume transparent black

                // bottom left
                if (ysi < height-1) {
                    w = wx1 * wy;
                    idx = getPixel(xsi, ysi + 1) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    cyt += (cy[idx] & 0xff) * w;
                } // else assume transparent black

                // bottom right
                if ((ysi < height - 1) && (xsi < width - 1)) {
                    w = wx * wy;
                    idx = getPixel(xsi + 1, ysi + 1) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    cyt += (cy[idx] & 0xff) * w;
                } // else assume transparent black

                int ati = (int)at;
                int cyti = (int)cyt;

                // find color index in palette

                // already known ?
                int colIdx = lastColIdx;
                if (ati != lastA || cyti != lastCY ) {
                    // determine index in target
                    if (ati < alphaThr) {
                        colIdx = 0; // transparent color
                    } else {
                        colIdx = 1; // default: lightest color
                        for (int threshold : lumThr) {
                            if (cyti > threshold) {
                                break;
                            }
                            colIdx++; // try next darker color
                        }
                    }
                    // remember
                    lastA = ati;
                    lastCY = cyti;
                    lastColIdx = colIdx;
                }
                // write target pixel
                trg.setPixel(xt, yt, (byte)colIdx);
            }
        }
        return trg;
    }

    /**
     * Scales a palettized Bitmap (where each palette entry has individual alpha) using a given scaling filter
     * to a Bitmap with N color palette, where:
     * index0 = transparent, index1 = light color, ... , indexN-2 = dark color, indexN-1 = black
     * @param sizeX		Target width
     * @param sizeY		Target height
     * @param pal 		Palette of the source Bitmap
     * @param alphaThr	Threshold for alpha (transparency), lower = more transparent
     * @param lumThr    Threshold for luminances. For N-1 luminances, N-2 thresholds are needed
     *                  lumThr[0] is the threshold for the lightest color (-> idx 1)
     *                  lumThr[N-2] is the threshold for the darkest color (-> idx N-1)
     * @param f Filter for scaling
     * @return Scaled Bitmap which uses a fixed frame Palette.
     */
    public Bitmap scaleFilterLm(final int sizeX, final int sizeY, final Palette pal, final int alphaThr, final int lumThr[], final ResampleFilter f) {
        final int[] trg = new FilterOp(f, sizeX, sizeY).filter(this, pal);

        Bitmap bm = new Bitmap(sizeX, sizeY);

        // select nearest colors in existing palette
        HashMap<Integer, Integer> p = new HashMap<Integer, Integer>();

        for (int i = 0; i < trg.length; i++) {
            int color = trg[i];
            int colIdx;
            Integer idxEx = p.get(color);
            if (idxEx != null) {
                colIdx = idxEx;
            }
            else {
                int alpha = (color >> 24) & 0xff;
                int red   = (color >> 16) & 0xff;
                int green = (color >>  8) & 0xff;
                int blue  =  color        & 0xff;
                int cyp   = RGB2YCbCr(red, green, blue, false)[0];

                // determine index in target
                if (alpha < alphaThr) {
                    colIdx = 0; // transparent color
                } else {
                    colIdx = 1; // default: lightest color
                    for (int threshold : lumThr) {
                        if (cyp > threshold) {
                            break;
                        }
                        colIdx++; // try next darker color
                    }
                }
                p.put(color, colIdx);
            }
            // write target pixel
            bm.buffer[i] = (byte)colIdx;
        }
        return bm;
    }

    /** Scales a palettized Bitmap to a Bitmap with the same palette using bilinear filtering.
     * @param sizeX Target width
     * @param sizeY Target height
     * @param pal   Palette of the source Bitmap
     * @return Scaled Bitmap which uses the same Palette as the source Bitmap.
     */
    public Bitmap scaleBilinear(final int sizeX, final int sizeY, final Palette pal) {
        final byte[] r = pal.getR();
        final byte[] g = pal.getG();
        final byte[] b = pal.getB();
        final byte[] a = pal.getAlpha();

        final double scaleX = (double)(width - 1) / (sizeX - 1);
        final double scaleY = (double)(height - 1) / (sizeY - 1);

        int lastR = 0;
        int lastG = 0;
        int lastB = 0;
        int lastA = 0;
        int lastColIdx = pal.getIndexOfMostTransparentPaletteEntry();

        final Bitmap trg = new Bitmap(sizeX, sizeY);

        for (int yt = 0; yt < sizeY; yt++) {
            double ys = yt * scaleY; // source coordinate
            int ysi = (int)ys;
            double wy = (ys - ysi);
            double wy1 = 1.0 - wy;

            for (int xt = 0; xt < sizeX; xt++) {
                double xs = xt * scaleX; // source coordinate
                int xsi = (int)xs;
                double wx = (xs - xsi); // weight factor

                double wx1 = 1.0 - wx;

                // interpolate pixel

                // top left
                double w = wx1 * wy1;
                int idx = getPixel(xsi, ysi) & 0xff;
                double at = (a[idx] & 0xff) * w;
                double rt = (r[idx] & 0xff) * w;
                double gt = (g[idx] & 0xff) * w;
                double bt = (b[idx] & 0xff) * w;

                // top right
                if (xsi < width - 1) {
                    w = wx * wy1;
                    idx = getPixel(xsi + 1, ysi) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    rt += (r[idx] & 0xff) * w;
                    gt += (g[idx] & 0xff) * w;
                    bt += (b[idx] & 0xff) * w;
                }  // else assume transparent black

                // bottom left
                if (ysi < height - 1) {
                    w = wx1 * wy;
                    idx = getPixel(xsi, ysi + 1) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    rt += (r[idx] & 0xff) * w;
                    gt += (g[idx] & 0xff) * w;
                    bt += (b[idx] & 0xff) * w;
                } // else assume transparent black

                // bottom right
                if ((ysi < height - 1) && (xsi < width - 1)) {
                    w = wx * wy;
                    idx = getPixel(xsi + 1, ysi + 1) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    rt += (r[idx] & 0xff) * w;
                    gt += (g[idx] & 0xff) * w;
                    bt += (b[idx] & 0xff) * w;

                } // else assume transparent black

                int ati = (int)(at + 0.5);
                int rti = (int)(rt + 0.5);
                int gti = (int)(gt + 0.5);
                int bti = (int)(bt + 0.5);

                // find color index in palette

                // already known ?
                int colIdx = lastColIdx;
                if (ati != lastA || rti != lastR || gti != lastG || bti!=lastB) {
                    int minDistance = 0xffffff; // init > 0xff*0xff*4 = 0x03f804
                    for (idx = 0; idx < pal.getSize(); idx++) {
                        // distance vector (skip sqrt)
                        int ad = ati - (a[idx] & 0xff);
                        int rd = rti - (r[idx] & 0xff);
                        int gd = gti - (g[idx] & 0xff);
                        int bd = bti - (b[idx] & 0xff);
                        int distance = rd * rd + gd * gd + bd * bd + ad * ad;
                        // new minimum distance ?
                        if ( distance < minDistance) {
                            colIdx = idx;
                            minDistance = distance;
                            if (minDistance == 0) {
                                break;
                            }
                        }
                    }
                    // remember values
                    lastA = ati;
                    lastR = rti;
                    lastG = gti;
                    lastB = bti;
                    lastColIdx = colIdx;
                }
                // write target pixel
                trg.setPixel(xt, yt, (byte)colIdx);

            }
        }
        return trg;
    }

    /** Scales a palettized Bitmap to a Bitmap with a new quantized Palette using bilinear filtering.
     * @param sizeX  Target width
     * @param sizeY  Target height
     * @param pal    Palette of the source Bitmap
     * @param dither True: apply dithering
     * @return Scaled Bitmap and new Palette
     */
    public BitmapWithPalette scaleBilinear(final int sizeX, final int sizeY, final Palette pal, final boolean dither) {
        final byte[] r = pal.getR();
        final byte[] g = pal.getG();
        final byte[] b = pal.getB();
        final byte[] a = pal.getAlpha();

        final double scaleX = (double)(width - 1) / (sizeX - 1);
        final double scaleY = (double)(height - 1) / (sizeY - 1);

        final int[] trg = new int[sizeX * sizeY];

        for (int yt = 0; yt < sizeY; yt++) {
            double ys = yt * scaleY; // source coordinate
            int ysi = (int)ys;
            double wy = (ys - ysi);
            double wy1 = 1.0 - wy;
            int ofsY = yt * sizeX;

            for (int xt = 0; xt < sizeX; xt++) {
                double xs = xt * scaleX; // source coordinate
                int xsi = (int)xs;
                double wx = (xs - xsi); // weight factor

                int idx;

                // interpolate pixel
                double wx1 = 1.0 - wx;
                int x,y;

                // top left
                double w = wx1 * wy1;
                idx = getPixel(xsi, ysi) & 0xff;
                double at = (a[idx] & 0xff) * w;
                double rt = (r[idx] & 0xff) * w;
                double gt = (g[idx] & 0xff) * w;
                double bt = (b[idx] & 0xff) * w;

                // top right
                x = xsi + 1;
                if (x < width) {
                    w = wx * wy1;
                    idx = getPixel(x, ysi) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    rt += (r[idx] & 0xff) * w;
                    gt += (g[idx] & 0xff) * w;
                    bt += (b[idx] & 0xff) * w;
                }  // else assume transparent black

                // bottom left
                y = ysi + 1;
                if (y < height) {
                    w = wx1 * wy;
                    idx = getPixel(xsi, y) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    rt += (r[idx] & 0xff) * w;
                    gt += (g[idx] & 0xff) * w;
                    bt += (b[idx] & 0xff) * w;
                } // else assume transparent black

                // bottom right
                x = xsi + 1;
                y = ysi + 1;
                if ((x < width) && (y < height)) {
                    w = wx * wy;
                    idx = getPixel(x, y) & 0xff;
                    at += (a[idx] & 0xff) * w;
                    rt += (r[idx] & 0xff) * w;
                    gt += (g[idx] & 0xff) * w;
                    bt += (b[idx] & 0xff) * w;
                } // else assume transparent black

                int ati = (int)(at);
                int rti = (int)(rt);
                int gti = (int)(gt);
                int bti = (int)(bt);

                trg[xt + ofsY] = ((ati << 24) | (rti << 16) | (gti << 8) | bti);
            }
        }
        // quantize image
        QuantizeFilter qf = new QuantizeFilter();
        final Bitmap bm = new Bitmap(sizeX, sizeY);
        int[] ct = qf.quantize(trg, bm.buffer, sizeX, sizeY, 255, dither, dither);
        int size = ct.length;
        if (size > 255) {
            size = 255;
            logger.warn("Quantizer failed.\n");
        }
        // create palette
        Palette trgPal = new Palette(256);
        for (int i = 0; i < size; i++) {
            trgPal.setARGB(i,ct[i]);
        }

        return new BitmapWithPalette(bm, trgPal);
    }

    /** Scales a palettized Bitmap to a Bitmap with the same Palette using a given scaling filter.
     * @param sizeX Target width
     * @param sizeY Target height
     * @param pal   Palette of the source Bitmap
     * @param f     Filter for scaling
     * @return Scaled Bitmap which uses the same Palette as the source Bitmap.
     */
    public Bitmap scaleFilter(final int sizeX, final int sizeY, final Palette pal, final ResampleFilter f) {
        final byte[] r = pal.getR();
        final byte[] g = pal.getG();
        final byte[] b = pal.getB();
        final byte[] a = pal.getAlpha();

        final int[] trg = new FilterOp(f, sizeX, sizeY).filter(this, pal);

        final Bitmap bm = new Bitmap(sizeX, sizeY);

        // select nearest colors in existing palette
        HashMap<Integer, Integer> p = new HashMap<Integer, Integer>();

        for (int i = 0; i < trg.length; i++) {
            int color = trg[i];
            int colIdx;
            Integer idxEx = p.get(color);
            if (idxEx != null) {
                colIdx = idxEx;
            } else {
                colIdx = 0;
                int minDistance = 0xffffff; // init > 0xff*0xff*4 = 0x03f804
                int alpha = (color >> 24) & 0xff;
                int red   = (color >> 16) & 0xff;
                int green = (color >>  8) & 0xff;
                int blue  =  color        & 0xff;
                for (int idx = 0; idx < pal.getSize(); idx++) {
                    // distance vector (skip sqrt)
                    int ad = alpha - (a[idx] & 0xff);
                    int rd = red   - (r[idx] & 0xff);
                    int gd = green - (g[idx] & 0xff);
                    int bd = blue  - (b[idx] & 0xff);
                    int distance = rd * rd + gd * gd + bd * bd + ad * ad;
                    // new minimum distance ?
                    if ( distance < minDistance) {
                        colIdx = idx;
                        minDistance = distance;
                        if (minDistance == 0) {
                            break;
                        }
                    }
                }
                p.put(color, colIdx);
            }
            // write target pixel
            bm.buffer[i] = (byte)colIdx;
        }
        return bm;
    }

    /** Scales a palettized Bitmap to a Bitmap with a new quantized Palette using a given scaling filter.
     * @param sizeX  Target width
     * @param sizeY  Target height
     * @param pal    Palette of the source Bitmap
     * @param f      Filter for scaling
     * @param dither True: apply dithering
     * @return Scaled Bitmap and new Palette
     */
    public BitmapWithPalette scaleFilter(final int sizeX, final int sizeY, final Palette pal, final ResampleFilter f, final boolean dither) {
        final int[] trg = new FilterOp(f, sizeX, sizeY).filter(this, pal);

        // quantize image
        QuantizeFilter qf = new QuantizeFilter();
        final Bitmap bm = new Bitmap(sizeX, sizeY);
        int[] ct = qf.quantize(trg, bm.buffer, sizeX, sizeY, 255, dither, dither);
        int size = ct.length;
        if (size > 255) {
            size = 255;
            logger.warn("Quantizer failed.\n");
        }
        // create palette
        Palette trgPal = new Palette(256);
        for (int i = 0; i < size; i++) {
            trgPal.setARGB(i, ct[i]);
        }

        return new BitmapWithPalette(bm, trgPal);
    }

    public int[] toARGB(Palette pal) {
        int[] argbValues = new int[buffer.length];
        for (int i = 0; i < argbValues.length; i++) {
            argbValues[i] = pal.getARGB(buffer[i] & 0xff);
        }
        return argbValues;
    }

    public Bitmap crop(int xOffset, int yOffset, int croppedBitmapWidth, int croppedBitmapHeight) {
        Bitmap bitmap = new Bitmap(croppedBitmapWidth, croppedBitmapHeight);

        int yOfsSrc = yOffset * width;
        int yOfsTrg = 0;
        for (int i = 0; i < croppedBitmapHeight; i++, yOfsSrc += width, yOfsTrg += croppedBitmapWidth) {
            for (int xTrg = 0; xTrg < croppedBitmapWidth; xTrg++) {
                bitmap.buffer[xTrg + yOfsTrg] = buffer[xOffset + xTrg + yOfsSrc];
            }
        }
        return bitmap;
    }

    public BitmapBounds getCroppingBounds(byte[] alpha, int alphaThreshold) {
        int xMin, xMax, yMin, yMax;

        // search lower bound
        yMax = height - 1;
        int yOfs = yMax * width;
        for (int y = height - 1; y > 0; y--, yOfs -= width) {
            yMax = y;
            if (isRowWithColorAboveAlphaThreshold(yOfs, alpha, alphaThreshold)) {
                break;
            }
        }

        // search upper bound
        yMin = 0;
        yOfs = 0;
        for (int y = 0; y < yMax; y++, yOfs += width) {
            yMin = y;
            if (isRowWithColorAboveAlphaThreshold(yOfs, alpha, alphaThreshold)) {
                break;
            }
        }

        // search right bound
        xMax = width-1;
        for (int x = width - 1; x > 0; x--) {
            xMax = x;
            if (isColumnWithColorAboveAlphaThreshold(x, yMin, yMax, alpha, alphaThreshold)) {
                break;
            }
        }

        // search left bound
        xMin = 0;
        for (int x = 0; x < xMax; x++) {
            xMin = x;
            if (isColumnWithColorAboveAlphaThreshold(x, yMin, yMax, alpha, alphaThreshold)) {
                break;
            }
        }

        return new BitmapBounds(xMin, xMax, yMin, yMax);
    }

    private boolean isRowWithColorAboveAlphaThreshold(int yOfs, byte[] alpha, int alphaThreshold) {
        for (int x = 0; x < width; x++) {
            if (isColorAboveAlphaThreshold(yOfs + x, alpha, alphaThreshold)) {
                return true;
            }
        }
        return false;
    }

    private boolean isColumnWithColorAboveAlphaThreshold(int x, int yMin, int yMax, byte[] alpha, int alphaThreshold) {
        int yOfs = yMin * width;
        for (int i = yMin; i < yMax; i++, yOfs += width) {
            if (isColorAboveAlphaThreshold(yOfs + x, alpha, alphaThreshold)) {
                return true;
            }
        }
        return false;
    }

    private boolean isColorAboveAlphaThreshold(int bufferIndex, byte[] alpha, int alphaThreshold) {
        int idx = buffer[bufferIndex] & 0xff;
        return (alpha[idx] & 0xff) >= alphaThreshold;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    public byte[] getInternalBuffer() {
        return buffer;
    }

    private byte getPixel(int x, int y) {
        return buffer[x + width * y];
    }

    private void setPixel(int x, int y, byte color) {
        buffer[x + width * y] = color;
    }
} public class Palette {
    /** Number of palette entries */
    private final int size;
    /** Byte buffer for RED info */
    private final byte[] r;
    /** Byte buffer for GREEN info */
    private final byte[] g;
    /** Byte buffer for BLUE info */
    private final byte[] b;
    /** Byte buffer for alpha info */
    private final byte[] a;
    /** Byte buffer for Y (luminance) info */
    private final byte[] y;
    /** Byte buffer for Cb (chrominance blue) info */
    private final byte[] cb;
    /** Byte buffer for Cr (chrominance red) info */
    private final byte[] cr;
    /** Use BT.601 color model instead of BT.709 */
    private final boolean useBT601;

    /**
     * Initializes palette with transparent black (RGBA: 0x00000000)
     * @param size Number of palette entries
     */
    public Palette(int size, boolean useBT601) {
        this.size = size;
        this.useBT601 = useBT601;
        r  = new byte[size];
        g  = new byte[size];
        b  = new byte[size];
        a  = new byte[size];
        y  = new byte[size];
        cb = new byte[size];
        cr = new byte[size];

        // set at least all alpha values to invisible
        int[] yCbCr = RGB2YCbCr(0, 0, 0, useBT601);
        for (int i=0; i < size; i++) {
            a[i]  = 0;
            r[i]  = 0;
            g[i]  = 0;
            b[i]  = 0;
            y[i]  = (byte)yCbCr[0];
            cb[i] = (byte)yCbCr[1];
            cr[i] = (byte)yCbCr[2];
        }
    }

    /**
     * Initializes a palette with transparent black (RGBA: 0x00000000)
     */
    public Palette(int size) {
        this(size, false);
    }

    public Palette(byte[] red, byte[] green, byte[] blue, byte[] alpha, boolean useBT601) {
        size = red.length;
        this.useBT601 = useBT601;
        r  = new byte[size];
        g  = new byte[size];
        b  = new byte[size];
        a  = new byte[size];
        y  = new byte[size];
        cb = new byte[size];
        cr = new byte[size];

        int yCbCr[];
        for (int i=0; i<size; i++) {
            a[i]  = alpha[i];
            r[i]  = red[i];
            g[i]  = green[i];
            b[i]  = blue[i];
            yCbCr = RGB2YCbCr(r[i]&0xff, g[i]&0xff, b[i]&0xff, useBT601);
            y[i]  = (byte)yCbCr[0];
            cb[i] = (byte)yCbCr[1];
            cr[i] = (byte)yCbCr[2];
        }
    }

    /**
     * Constructs a palette from red, green blue and alpha buffers using BT.709
     */
    public Palette(byte[] red, byte[] green, byte[] blue, byte[] alpha) {
        this(red, green, blue, alpha, false);
    }

    public Palette(Palette palette) {
        size = palette.getSize();
        useBT601 = palette.usesBT601();
        r  = new byte[size];
        g  = new byte[size];
        b  = new byte[size];
        a  = new byte[size];
        y  = new byte[size];
        cb = new byte[size];
        cr = new byte[size];

        for (int i = 0; i < size; i++) {
            a[i]  = palette.a[i];
            r[i]  = palette.r[i];
            g[i]  = palette.g[i];
            b[i]  = palette.b[i];
            y[i]  = palette.y[i];
            cb[i] = palette.cb[i];
            cr[i] = palette.cr[i];
        }
    }

    public ColorModel getColorModel() {
        return new IndexColorModel(8, size, r, g, b, a);
    }

    public void setColor(int index, Color c) {
        setRGB(index, c.getRed(), c.getGreen(), c.getBlue());
        setAlpha(index, c.getAlpha());
    }

    public void setARGB(int index, int c) {
        setColor(index, new Color(c, true));
    }

    public Color getColor(int index) {
        return new Color(r[index] & 0xff, g[index] & 0xff, b[index] & 0xff, a[index] & 0xff);
    }

    public int getARGB(int index) {
        return ((a[index] & 0xff) << 24) | ((r[index] & 0xff) << 16) | (( g[index] & 0xff) << 8) | (b[index] & 0xff) ;
    }

    public void setRGB(int index, int red, int green, int blue) {
        r[index] = (byte)red;
        g[index] = (byte)green;
        b[index] = (byte)blue;

        int yCbCr[] = RGB2YCbCr(red, green, blue, useBT601);
        y[index]  = (byte)yCbCr[0];
        cb[index] = (byte)yCbCr[1];
        cr[index] = (byte)yCbCr[2];
    }

    public void setYCbCr(int index, int yn, int cbn, int crn) {
        y[index]  = (byte)yn;
        cb[index] = (byte)cbn;
        cr[index] = (byte)crn;

        int rgb[] = ColorSpaceUtils.YCbCr2RGB(yn, cbn, crn, useBT601);
        r[index] = (byte)rgb[0];
        g[index] = (byte)rgb[1];
        b[index] = (byte)rgb[2];
    }

    public void setAlpha(int index, int alpha) {
        a[index] = (byte)alpha;
    }

    public int getAlpha(int index) {
        return a[index] & 0xff;
    }

    public byte[] getAlpha() {
        return Arrays.copyOf(a, a.length);
    }

    public int[] getRGB(int index) {
        int rgb[] = new int[3];
        rgb[0] = r[index] & 0xff;
        rgb[1] = g[index] & 0xff;
        rgb[2] = b[index] & 0xff;
        return rgb;
    }

    public int[] getYCbCr(final int index) {
        int yCbCr[] = new int[3];
        yCbCr[0] = y[index]  & 0xff;
        yCbCr[1] = cb[index] & 0xff;
        yCbCr[2] = cr[index] & 0xff;
        return yCbCr;
    }

    public byte[] getR() {
        return Arrays.copyOf(r, r.length);
    }

    public byte[] getG() {
        return Arrays.copyOf(g, g.length);
    }

    public byte[] getB() {
        return Arrays.copyOf(b, b.length);
    }

    public byte[] getY() {
        return Arrays.copyOf(y, y.length);
    }

    public byte[] getCb() {
        return Arrays.copyOf(cb, cb.length);
    }

    public byte[] getCr() {
        return Arrays.copyOf(cr, cr.length);
    }

    public int getSize() {
        return size;
    }

    public int getIndexOfMostTransparentPaletteEntry() {
        int transpIdx = 0;
        int minAlpha = 0x100;
        for (int i = 0; i < size; i++ ) {
            if ((a[i] & 0xff) < minAlpha) {
                minAlpha = a[i] & 0xff;
                transpIdx = i;
                if (minAlpha == 0) {
                    break;
                }
            }
        }
        return transpIdx;
    }

    public boolean usesBT601() {
        return useBT601;
    }
} public final class Configuration {

    public static final Color OK_BACKGROUND = UIManager.getColor("TextField.background");
    public static final Color WARN_BACKGROUND = new Color(0xffffffc0);
    public static final Color ERROR_BACKGROUND = new Color(0xffe1acac);

    public static final boolean CONVERT_RESOLUTION_BY_DEFAULT = false;
    public static final boolean CONVERT_FRAMERATE_BY_DEFAULT = false;
    public static final int DEFAULT_PTS_DELAY = 0;
    public static final boolean FIX_SHORT_FRAMES_BY_DEFAULT = false;
    public static final int DEFAULT_MIN_DISPLAY_TIME_PTS = 90 * 500;
    public static final boolean APPLY_FREE_SCALE_BY_DEFAULT = false;
    public static final double DEFAULT_FREE_SCALE_FACTOR_X = 1.0;
    public static final double DEFAULT_FREE_SCALE_FACTOR_Y = 1.0;
    public static final double MIN_FREE_SCALE_FACTOR = 0.5;
    public static final double MAX_FREE_SCALE_FACTOR = 2.0;
    public static final double DEFAULT_SOURCE_FRAMERATE = Framerate.FPS_23_976.getValue();  //TODO: dumb default
    public static final double DEFAULT_TARGET_FRAMERATE = Framerate.PAL.getValue(); //TODO: dumb default
    public static final Resolution DEFAULT_TARGET_RESOLUTION = Resolution.PAL; //TODO: dumb default
    public static final ForcedFlagState DEFAULT_FORCED_FLAG_STATE = ForcedFlagState.KEEP;
    public static final int DEFAULT_ALPHA_CROP_THRESHOLD = 14;
    public static final int DEFAULT_ALPHA_THRESHOLD = 80;
    public static final int DEFAULT_LUMINANCE_MED_HIGH_THRESHOLD = 210;
    public static final int DEFAULT_LUMINANCE_LOW_MED_THRESHOLD = 160;
    public static final int DEFAULT_MOVE_Y_OFFSET = 10;
    public static final int DEFAULT_MOVE_X_OFFSET = 10;
    public static final int DEFAULT_CROP_LINE_COUNT = 0;
    //Two equal captions are merged of they are closer than 200ms (0.2*90000 = 18000)
    public static final int DEFAULT_MERGE_PTS_DIFF = 18000;
    public static final OutputMode DEFAULT_OUTPUT_MODE = OutputMode.VOBSUB;

    private boolean convertResolution = CONVERT_RESOLUTION_BY_DEFAULT;
    private boolean convertFPS = CONVERT_FRAMERATE_BY_DEFAULT;
    private int delayPTS = DEFAULT_PTS_DELAY;
    private boolean cliMode = true;
    private boolean fixShortFrames = FIX_SHORT_FRAMES_BY_DEFAULT;
    private int minTimePTS = DEFAULT_MIN_DISPLAY_TIME_PTS;
    private boolean applyFreeScale = APPLY_FREE_SCALE_BY_DEFAULT;
    private double freeScaleFactorX = DEFAULT_FREE_SCALE_FACTOR_X;
    private double freeScaleFactorY = DEFAULT_FREE_SCALE_FACTOR_Y;
    private double fpsSrc = DEFAULT_SOURCE_FRAMERATE;
    private double fpsTrg = DEFAULT_TARGET_FRAMERATE;
    private boolean fpsSrcCertain;
    private Resolution outputResolution = DEFAULT_TARGET_RESOLUTION;
    private int languageIdx;
    private boolean exportForced;

    private int cropOffsetY = DEFAULT_CROP_LINE_COUNT;
    private ForcedFlagState forceAll = DEFAULT_FORCED_FLAG_STATE;
    private boolean swapCrCb;
    private CaptionMoveModeX moveModeX = CaptionMoveModeX.KEEP_POSITION;
    private CaptionMoveModeY moveModeY = CaptionMoveModeY.KEEP_POSITION;
    private int moveOffsetX = DEFAULT_MOVE_X_OFFSET;
    private int moveOffsetY = DEFAULT_MOVE_Y_OFFSET;
    private boolean moveCaptions;
    /** Factor to calculate height of one cinemascope bar from screen height */
    private double cineBarFactor = 5.0/42;
    private StreamID currentStreamID = StreamID.UNKNOWN;
    private boolean keepFps;

    private static final int RECENT_FILE_COUNT = 5;
    private static final String CONFIG_FILE = "bdsup2sup.ini";
    private static final Configuration INSTANCE = new Configuration();
    private final String configFilePath;
    private List<String> recentFiles;
    private int[] luminanceThreshold = {DEFAULT_LUMINANCE_MED_HIGH_THRESHOLD, DEFAULT_LUMINANCE_LOW_MED_THRESHOLD};
    private int alphaThreshold = DEFAULT_ALPHA_THRESHOLD;

    private Props props;

    private Configuration() {
        configFilePath = getConfigFilePath();
        props = new Props();
    }

    public void load() {
        readConfigFile();
        loadConfig();
    }

    private void readConfigFile() {
        props.clear();
        props.setHeader(Constants.APP_NAME + " " + Constants.APP_VERSION + " settings - don't modify manually");
        props.load(configFilePath);
    }

    private void loadConfig() {
        loadRecentFiles();

        convertResolution = loadConvertResolution();
        outputResolution = loadOutputResolution();
        convertFPS = loadConvertFPS();
        fpsSrc = loadFpsSrc();
        fpsTrg = loadFpsTrg();
        delayPTS = loadDelayPTS();
        fixShortFrames = loadFixShortFrames();
        minTimePTS = loadMinTimePTS();
        applyFreeScale = loadApplyFreeScale();
        freeScaleFactorX = loadFreeScaleFactorX();
        freeScaleFactorY = loadFreeScaleFactorY();
        forceAll = loadForceAll();
    }

    public void storeConfig() {
        if (!cliMode) {
            props.save(configFilePath);
        }
    }

    private void loadRecentFiles() {
        recentFiles = new ArrayList<String>();
        int i = 0;
        String filename;
        while (i < RECENT_FILE_COUNT && (filename = props.get("recent_" + i, "")).length() > 0) {
            if (new File(filename).exists()) {
                recentFiles.add(filename);
            }
            i++;
        }
    }

    public static Configuration getInstance() {
        return INSTANCE;
    }

    public String getConfigFilePath() {
        if (PlatformUtils.isLinux()) {
            String xdgConfigHome = System.getenv("XDG_CONFIG_HOME");
            File configFileDir = new File((xdgConfigHome != null ? xdgConfigHome : System.getProperty("user.home") + "/.config") + "/bdsup2sub");
            if (!configFileDir.exists()) {
                configFileDir.mkdir();
            }
            return configFileDir + "/" + CONFIG_FILE;
        } else if (PlatformUtils.isMac()) {
            File configFileDir = new File(System.getProperty("user.home") + "/Library/Application Support/bdsup2sub");
            if (!configFileDir.exists()) {
                configFileDir.mkdir();
            }
            return configFileDir + "/" + CONFIG_FILE;
        } else {
            File configFileDir = new File(System.getProperty("user.home") + "/bdsup2sub");
            if (!configFileDir.exists()) {
                configFileDir.mkdir();
            }
            return configFileDir + "/" + CONFIG_FILE;
        }
    }
    
    public boolean isCliMode() {
        return cliMode;
    }

    public void setCliMode(boolean cliMode) {
        this.cliMode = cliMode;
    }

    public boolean isVerbose() {
        return props.get("verbose", false);
    }

    public void setVerbose(boolean verbose) {
        props.set("verbose", verbose);
    }

    /**
     * Write PGCEdit palette file on export.
     */
    public boolean getWritePGCEditPalette() {
        return props.get("writePGCEditPal", false);
    }

    public void setWritePGCEditPalette(boolean writePGCEditPalette) {
        props.set("writePGCEditPal", writePGCEditPalette);
    }

    /**
     * Get maximum time difference for merging captions.
     */
    public int getMergePTSdiff() {
        return props.get("mergePTSdiff", DEFAULT_MERGE_PTS_DIFF);
    }

    public void setMergePTSdiff(int mergePTSdiff) {
        props.set("mergePTSdiff", mergePTSdiff);
    }

    /**
     * Get alpha threshold for cropping.
     */
    public int getAlphaCrop() {
        return props.get("alphaCrop", DEFAULT_ALPHA_CROP_THRESHOLD);
    }

    public void setAlphaCrop(int alphaCrop) {
        props.set("alphaCrop", alphaCrop);
    }

    /**
     * Fix completely invisible subtitles due to alpha=0 (SUB/IDX and SUP/IFO import only).
     */
    public boolean getFixZeroAlpha() {
        return props.get("fixZeroAlpha", false);
    }

    public void setFixZeroAlpha(boolean fixZeroAlpha) {
        props.set("fixZeroAlpha", fixZeroAlpha);
    }

    public ScalingFilter getScalingFilter() {
        ScalingFilter defaultScalingFilter = ScalingFilter.BILINEAR;
        try {
            return ScalingFilter.valueOf(props.get("filter", defaultScalingFilter.name()));
        } catch (IllegalArgumentException ex) {
            return defaultScalingFilter;
        }
    }

    public void setScalingFilter(ScalingFilter filter) {
        props.set("filter", filter.name());
    }

    /**
     * Get palette creation mode.
     */
    public PaletteMode getPaletteMode() {
        PaletteMode defaultPaletteMode = PaletteMode.CREATE_NEW;
        try {
            return PaletteMode.valueOf(props.get("paletteMode", defaultPaletteMode.name()));
        } catch (IllegalArgumentException ex) {
            return defaultPaletteMode;
        }
    }

    public void setPaletteMode(PaletteMode paletteMode) {
        props.set("paletteMode", paletteMode.name());
    }

    public OutputMode getOutputMode() {
        try {
            return OutputMode.valueOf(props.get("outputMode", DEFAULT_OUTPUT_MODE.name()));
        } catch (IllegalArgumentException ex) {
            return DEFAULT_OUTPUT_MODE;
        }
    }
    
    public void setOutputMode(OutputMode outputMode) {
        props.set("outputMode", outputMode.name());
    }

    public String getLoadPath() {
        return props.get("loadPath", "");
    }

    public void setLoadPath(String loadPath) {
        props.set("loadPath", loadPath);
    }

    public String getColorProfilePath() {
        return props.get("colorPath", "");
    }

    public void setColorProfilePath(String colorProfilePath) {
        props.set("colorPath", colorProfilePath);
    }

    public Dimension getMainWindowSize() {
        return new Dimension(props.get("frameWidth", 800), props.get("frameHeight", 600));
    }

    public void setMainWindowSize(Dimension dimension) {
        props.set("frameWidth", dimension.width);
        props.set("frameHeight", dimension.height);
    }

    public Point getMainWindowLocation() {
        return new Point(props.get("framePosX", -1), props.get("framePosY", -1));
    }

    public void setMainWindowLocation(Point location) {
        props.set("framePosX", location.x);
        props.set("framePosY", location.y);
    }

    public List<String> getRecentFiles() {
        return recentFiles;
    }

    public void addToRecentFiles(String filename) {
        int index = recentFiles.indexOf(filename);
        if (index != -1) {
            recentFiles.remove(index);
            recentFiles.add(0, filename);
        } else {
            recentFiles.add(0, filename);
            if (recentFiles.size() > RECENT_FILE_COUNT) {
                recentFiles.remove(recentFiles.size() - 1);
            }
        }
        for (int i=0; i < recentFiles.size(); i++) {
            props.set("recent_" + i, recentFiles.get(i));
        }
    }

    public boolean getConvertResolution() {
        return convertResolution;
    }

    public void setConvertResolution(boolean convertResolution) {
        this.convertResolution = convertResolution;
    }

    public boolean loadConvertResolution() {
        return props.get("convertResolution", CONVERT_RESOLUTION_BY_DEFAULT);
    }

    public void storeConvertResolution(boolean convertResolution) {
        props.set("convertResolution", convertResolution);
    }

    /**
     * Get flag that tells whether or not to convert the frame rate.
     */
    public boolean getConvertFPS() {
        return convertFPS;
    }

    public void setConvertFPS(boolean convertFPS) {
        this.convertFPS = convertFPS;
    }

    public boolean loadConvertFPS() {
        return props.get("convertFPS", CONVERT_FRAMERATE_BY_DEFAULT);
    }

    public void storeConvertFPS(boolean convertFPS) {
        props.set("convertFPS", convertFPS);
    }

    /**
     * Get Delay to apply to target in 90kHz resolution.
     */
    public int getDelayPTS() {
        return delayPTS;
    }

    public void setDelayPTS(int delayPTS) {
        this.delayPTS = delayPTS;
    }

    public int loadDelayPTS() {
        return props.get("delayPTS", DEFAULT_PTS_DELAY);
    }

    public void storeDelayPTS(int delayPTS) {
        props.set("delayPTS", delayPTS);
    }

    /**
     * Set flag that tells whether to fix subpictures with a display time shorter than minTimePTS.
     */
    public boolean getFixShortFrames() {
        return fixShortFrames;
    }

    public void setFixShortFrames(boolean fixShortFrames) {
        this.fixShortFrames = fixShortFrames;
    }

    public boolean loadFixShortFrames() {
        return props.get("fixShortFrames", FIX_SHORT_FRAMES_BY_DEFAULT);
    }

    public void storeFixShortFrames(boolean fixShortFrames) {
        props.set("fixShortFrames",  fixShortFrames);
    }

    /**
     * Get minimum display time for subpictures in 90kHz resolution
     */
    public int getMinTimePTS() {
        return minTimePTS;
    }

    public void setMinTimePTS(int minTimePTS) {
        this.minTimePTS = minTimePTS;
    }

    public int loadMinTimePTS() {
        return props.get("minTimePTS", DEFAULT_MIN_DISPLAY_TIME_PTS);
    }

    public void storeMinTimePTS(int minTimePTS) {
        props.set("minTimePTS", minTimePTS);
    }

    public boolean getApplyFreeScale() {
        return applyFreeScale;
    }

    public void setApplyFreeScale(boolean applyFreeScale) {
        this.applyFreeScale = applyFreeScale;
    }

    public boolean loadApplyFreeScale() {
        return props.get("applyFreeScale", APPLY_FREE_SCALE_BY_DEFAULT);
    }

    public void storeApplyFreeScale(boolean applyFreeScale) {
        props.set("applyFreeScale", applyFreeScale);
    }

    public double getFreeScaleFactorX() {
        return freeScaleFactorX;
    }

    public double getFreeScaleFactorY() {
        return freeScaleFactorY;
    }

    public void setFreeScaleFactor(double x, double y) {
        if (x < MIN_FREE_SCALE_FACTOR) {
            x = MIN_FREE_SCALE_FACTOR;
        } else if (x > MAX_FREE_SCALE_FACTOR) {
            x = MAX_FREE_SCALE_FACTOR;
        }
        freeScaleFactorX = x;

        if (y < MIN_FREE_SCALE_FACTOR) {
            y = MIN_FREE_SCALE_FACTOR;
        } else if (y > MAX_FREE_SCALE_FACTOR) {
            y = MAX_FREE_SCALE_FACTOR;
        }
        freeScaleFactorY = y;
    }

    public double loadFreeScaleFactorX() {
        return props.get("freeScaleX", DEFAULT_FREE_SCALE_FACTOR_X);
    }

    public double loadFreeScaleFactorY() {
        return props.get("freeScaleY", DEFAULT_FREE_SCALE_FACTOR_Y);
    }

    public void storeFreeScaleFactor(double x, double y) {
        if (x < MIN_FREE_SCALE_FACTOR) {
            x = MIN_FREE_SCALE_FACTOR;
        } else if (x > MAX_FREE_SCALE_FACTOR) {
            x = MAX_FREE_SCALE_FACTOR;
        }
        props.set("freeScaleX", x);

        if (y < MIN_FREE_SCALE_FACTOR) {
            y = MIN_FREE_SCALE_FACTOR;
        } else if (y > MAX_FREE_SCALE_FACTOR) {
            y = MAX_FREE_SCALE_FACTOR;
        }
        props.set("freeScaleY", y);
    }

    public double getFPSSrc() {
        return fpsSrc;
    }

    public void setFpsSrc(double fpsSrc) {
        this.fpsSrc = fpsSrc;
    }

    public double loadFpsSrc() {
        return SubtitleUtils.getFps(props.get("fpsSrc", String.valueOf(DEFAULT_SOURCE_FRAMERATE)));
    }

    public void storeFPSSrc(double fpsSrc) {
        props.set("fpsSrc", fpsSrc);
    }

    public double getFpsTrg() {
        return fpsTrg;
    }

    public void setFpsTrg(double fpsTrg) {
        this.fpsTrg = fpsTrg;
        setDelayPTS((int)SubtitleUtils.syncTimePTS(getDelayPTS(), fpsTrg, fpsTrg));
        setMinTimePTS((int)SubtitleUtils.syncTimePTS(getMinTimePTS(), fpsTrg, fpsTrg));
    }

    public double loadFpsTrg() {
        return SubtitleUtils.getFps(props.get("fpsTrg", String.valueOf(DEFAULT_TARGET_FRAMERATE)));
    }

    public void storeFpsTrg(double fpsTrg) {
        props.set("fpsTrg", fpsTrg);
    }

    public boolean isFpsSrcCertain() {
        return fpsSrcCertain;
    }

    public void setFpsSrcCertain(boolean fpsSrcCertain) {
        this.fpsSrcCertain = fpsSrcCertain;
    }

    public Resolution getOutputResolution() {
        return outputResolution;
    }

    public void setOutputResolution(Resolution outputResolution) {
        this.outputResolution = outputResolution;
    }

    public Resolution loadOutputResolution() {
        try {
            return Resolution.valueOf(props.get("resolutionTrg", DEFAULT_TARGET_RESOLUTION.name()));
        } catch (IllegalArgumentException e) {
            return DEFAULT_TARGET_RESOLUTION;
        }
    }

    public void storeOutputResolution(Resolution outputResolution) {
        props.set("resolutionTrg", outputResolution.name());
    }

    public int getAlphaThreshold() {
        return alphaThreshold;
    }

    public void setAlphaThreshold(int alphaThreshold) {
        this.alphaThreshold = alphaThreshold;
    }

    /**
     * Array of luminance thresholds ( 0: med/high, 1: low/med )
     */
    public int[] getLuminanceThreshold() {
        return luminanceThreshold;
    }

    public void setLuminanceThreshold(int[] luminanceThreshold) {
        this.luminanceThreshold = luminanceThreshold;
    }

    /**
     * Index of language to be used for SUB/IDX export (also used for XML export)
     */
    public int getLanguageIdx() {
        return languageIdx;
    }

    public void setLanguageIdx(int languageIdx) {
        this.languageIdx = languageIdx;
    }

    /**
     * Flag that defines whether to export only subpictures marked as "forced"
     */
    public boolean isExportForced() {
        return exportForced;
    }

    public void setExportForced(boolean exportForced) {
        this.exportForced = exportForced;
    }

    public int getCropOffsetY() {
        return cropOffsetY;
    }

    public void setCropOffsetY(int cropOffsetY) {
        this.cropOffsetY = cropOffsetY;
    }

    public ForcedFlagState getForceAll() {
        return forceAll;
    }

    public ForcedFlagState loadForceAll() {
        try {
            return ForcedFlagState.valueOf(props.get("forceAll", DEFAULT_FORCED_FLAG_STATE.name()));
        } catch (IllegalArgumentException ex) {
            return DEFAULT_FORCED_FLAG_STATE;
        }
    }

    public void setForceAll(ForcedFlagState forceAll) {
        this.forceAll = forceAll;
    }

    public void storeForceAll(ForcedFlagState forceAll) {
        props.set("forceAll", forceAll.name());
    }

    public boolean isSwapCrCb() {
        return swapCrCb;
    }

    public void setSwapCrCb(boolean swapCrCb) {
        this.swapCrCb = swapCrCb;
    }

    public void setMoveModeY(CaptionMoveModeY moveModeY) {
        this.moveModeY = moveModeY;
        this.moveCaptions = (moveModeY != CaptionMoveModeY.KEEP_POSITION) || (moveModeX != CaptionMoveModeX.KEEP_POSITION);
    }

    public CaptionMoveModeY getMoveModeY() {
        return moveModeY;
    }

    public void setMoveModeX(CaptionMoveModeX moveModeX) {
        this.moveModeX = moveModeX;
        this.moveCaptions = (moveModeY != CaptionMoveModeY.KEEP_POSITION) || (moveModeX != CaptionMoveModeX.KEEP_POSITION);
    }

    public CaptionMoveModeX getMoveModeX() {
        return moveModeX;
    }

    public void setMoveOffsetY(int moveOffsetY) {
        this.moveOffsetY = moveOffsetY;
    }

    public int getMoveOffsetY() {
        return moveOffsetY;
    }

    public void setMoveOffsetX(int moveOffsetX) {
        this.moveOffsetX = moveOffsetX;
    }

    public int getMoveOffsetX() {
        return moveOffsetX;
    }

    /**
     * Get: keep move settings after loading a new stream
     * @return true: keep settings, false: ignore settings
     */
    public boolean getMoveCaptions() {
        return moveCaptions;
    }

    /**
     * Set: keep move settings after loading a new stream
     * @param moveCaptions true: keep settings, false; ignore settings
     */
    public void setMoveCaptions(boolean moveCaptions) {
        this.moveCaptions = moveCaptions;
    }

    public double getCineBarFactor() {
        return cineBarFactor;
    }

    public void setCineBarFactor(double cineBarFactor) {
        this.cineBarFactor = cineBarFactor;
    }

    public StreamID getCurrentStreamID() {
        return currentStreamID;
    }

    public void setCurrentStreamID(StreamID currentStreamID) {
        this.currentStreamID = currentStreamID;
    }

    public boolean isKeepFps() {
        return keepFps;
    }

    public void setKeepFps(boolean keepFps) {
        this.keepFps = keepFps;
    }
} public enum Framerate {
    FPS_23_976(24000.0/1001, 0x10), //24p
    FPS_23_975(23.975, 0x10),
    FPS_24(24.0, 0x20),
    PAL(25.0, 0x30),
    NTSC(30000.0/1001, 0x40),
    PAL_I(50.0,0x60),
    NTSC_I(60000.0/1001, 0x70);

    private final double value;
    private final int id;

    private Framerate(double value, int id) {
        this.value = value;
        this.id = id;
    }

    public double getValue() {
        return value;
    }

    public int getId() {
        return id;
    }

    public static double valueForId(int id) {
        for (Framerate framerate : Framerate.values()) {
            if (id == framerate.getId()) {
                return framerate.getValue();
            }
        }
        throw new IllegalArgumentException("Unsupported id: " + id);
    }

    public static int idForFramerate(double fps) {
        for (Framerate framerate : Framerate.values()) {
            if (fps == framerate.getValue()) {
                return framerate.getId();
            }
        }
        throw new IllegalArgumentException("Unsupported framerate: " + fps);
    }
} public final class Logger {

    private static final Configuration configuration = Configuration.getInstance();
    private static final Logger INSTANCE = new Logger();

    private int errorCount;
    private int warningCount;

    private MainFrameView mainFrame;

    private Logger() {
    }

    public static Logger getInstance() {
        return INSTANCE;
    }

    public void warn(String message) {
        warningCount++;
        final String msg = "WARNING: " + message;
        if (mainFrame != null) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    mainFrame.printToConsole(msg);
                }
            });
        } else {
            System.out.print(msg);
        }
    }

    public void error(String message) {
        errorCount++;
        final String msg = "ERROR: " + message;
        if (mainFrame != null) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    mainFrame.printToConsole(msg);
                }
            });
        } else {
            System.out.print(msg);
        }
    }

    public void trace(String message) {
        final String msg = message;
        if (configuration.isVerbose()) {
            if (mainFrame != null) {
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        mainFrame.printToConsole(msg);
                    }
                });
            } else {
                System.out.print(msg);
            }
        }
    }

    public void info(String message) {
        final String msg = message;
        if (mainFrame != null) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    mainFrame.printToConsole(msg);
                }
            });
        } else {
            System.out.print(msg);
        }
    }

    public int getErrorCount() {
        return errorCount;
    }

    public void resetErrorCounter() {
        errorCount = 0;
    }

    public int getWarningCount() {
        return warningCount;
    }

    public void resetWarningCounter() {
        warningCount = 0;
    }

    public void printWarningsAndErrorsAndResetCounters() {
        if (warningCount + errorCount > 0) {
            String message = "";
            if (warningCount > 0) {
                if (warningCount == 1) {
                    message += warningCount + " warning";
                } else {
                    message += warningCount + " warnings";
                }
            }
            if (warningCount > 0 && errorCount > 0) {
                message += " and ";
            }
            if (errorCount > 0) {
                if (errorCount == 1) {
                    message = errorCount + " error";
                } else {
                    message = errorCount + " errors";
                }
            }
            if (warningCount + errorCount < 3) {
                message = "There was " + message;
            } else {
                message = "There were " + message;
            }
            System.out.println(message);
        }
        resetWarningCounter();
        resetErrorCounter();
    }

    public void setMainFrame(MainFrameView mainFrame) {
        this.mainFrame = mainFrame;
    }
} public class SubPicture {

    /** width of subtitle image */
    private int imageWidth;
    /** height of subtitle image */
    private int imageHeight;
    /** upper left corner of subtitle x */
    private int xOffset;
    /** upper left corner of subtitle y */
    private int yOffset;

    /** screen width */
    private int width;
    /** screen height */
    private int height;
    /** start time in milliseconds */
    private long startTime;
    /** end time in milliseconds */
    private long endTime;
    /** if true, this is a forced subtitle */
    private boolean forced;
    /** composition number - increased at start and end PCS */
    private int compositionNumber;
    /** frame was already decoded */
    private boolean wasDecoded;

    /* the following fields are really only needed for editing */

    /** exclude from export? */
    private boolean excluded;
    /** list of erase patches */
    private List<ErasePatch> erasePatch = new ArrayList<ErasePatch>();

    public SubPicture() {
    }

    public SubPicture(SubPicture other) {
        /* Note that by using the getter functions
         * the internal values of a SubPictureBD are
         * copied into the plain members of the
         * SubPicture object.
         */
        this.imageWidth = other.getImageWidth();
        this.imageHeight = other.getImageHeight();
        this.xOffset = other.getXOffset();
        this.yOffset = other.getYOffset();
        this.imageWidth = other.imageWidth;
        this.imageHeight = other.imageHeight;
        this.xOffset = other.xOffset;
        this.yOffset = other.yOffset;

        this.width = other.width;
        this.height = other.height;
        this.startTime = other.startTime;
        this.endTime = other.endTime;
        this.forced = other.forced;
        this.compositionNumber = other.compositionNumber;
        this.wasDecoded = other.wasDecoded;
        this.excluded = other.excluded;
        if (other.erasePatch != null) {
            this.erasePatch = new ArrayList<ErasePatch>(other.erasePatch);
        }
    }

    public int getImageWidth() {
        return imageWidth;
    }

    public int getImageHeight() {
        return imageHeight;
    }

    public int getXOffset() {
        return xOffset;
    }

    public int getYOffset() {
        return yOffset;
    }

    public void setImageWidth(int w) {
        imageWidth = w;
    }

    public void setImageHeight(int h) {
        imageHeight = h;
    }

    public void setOfsX(int ofs) {
        xOffset = ofs;
    }

    public void setOfsY(int ofs) {
        yOffset = ofs;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public long getStartTime() {
        return startTime;
    }

    public void setStartTime(long startTime) {
        this.startTime = startTime;
    }

    public long getEndTime() {
        return endTime;
    }

    public void setEndTime(long endTime) {
        this.endTime = endTime;
    }

    public boolean isForced() {
        return forced;
    }

    public void setForced(boolean forced) {
        this.forced = forced;
    }

    public int getCompositionNumber() {
        return compositionNumber;
    }

    public void setCompositionNumber(int compositionNumber) {
        this.compositionNumber = compositionNumber;
    }

    public boolean isWasDecoded() {
        return wasDecoded;
    }

    public void setWasDecoded(boolean wasDecoded) {
        this.wasDecoded = wasDecoded;
    }

    public boolean isExcluded() {
        return excluded;
    }

    public void setExcluded(boolean excluded) {
        this.excluded = excluded;
    }

    public List<ErasePatch> getErasePatch() {
        return erasePatch;
    }

    public void setErasePatch(List<ErasePatch> erasePatch) {
        this.erasePatch = erasePatch;
    }
} public class QuantizeFilter  {

    /**
     * Floyd-Steinberg dithering matrix.
     */
    private static final int[] FS_MATRIX = {
         0, 0, 0,
         0, 0, 7,
         3, 5, 1,
    };
    private static final int SUM = 3 + 5 + 7 + 1;

    private boolean dither;
    private int numColors = 255;
    private boolean serpentine = true;

    /**
     * Set the number of colors to quantize to.
     * @param numColors Number of colors. The default is 256.
     */
    public void setNumColors(int numColors) {
        this.numColors = Math.min(Math.max(numColors, 8), 256);
    }

    /**
     * Clamp a value to the range 0..255
     * @param c Value to clamp
     * @return Clamped value
     */
    private static int clamp(final int c) {
        if (c < 0) {
            return 0;
        }
        if (c > 255) {
            return 255;
        }
        return c;
    }

    /**
     * Get the number of colors to quantize to.
     * @return Number of colors.
     */
    public int getNumColors() {
        return numColors;
    }

    /**
     * Set whether to use dithering or not. If not, the image is posterized.
     * @param dither True to use dithering
     */
    public void setDither(boolean dither) {
        this.dither = dither;
    }

    /**
     * Return the dithering setting
     * @return Current dithering setting
     */
    public boolean getDither() {
        return dither;
    }

    /**
     * Set whether to use a serpentine pattern for return or not. This can reduce 'avalanche' artifacts in the output.
     * @param serpentine True to use serpentine pattern
     */
    public void setSerpentine(boolean serpentine) {
        this.serpentine = serpentine;
    }

    /**
     * Return the serpentine setting
     * @return Current serpentine setting
     */
    public boolean getSerpentine() {
        return serpentine;
    }

    /**
     * Quantize picture
     * @param inPixels Array of RGBA pixels to quantize
     * @param outPixels Array with quantized palette entries
     * @param width Width of image
     * @param height Height of image
     * @param numColors Number of colors used
     * @param dither Use dithering?
     * @param serpentine Use serpentine for dithering?
     * @return Integer array containing palette information
     */
    public int[] quantize(int[] inPixels, byte[] outPixels, int width, int height, int numColors, boolean dither, boolean serpentine) {
        int count = width * height;
        OctTreeQuantizer quantizer = new OctTreeQuantizer();
        quantizer.setup(numColors);
        quantizer.addPixels(inPixels, 0, count);
        int[] table =  quantizer.buildColorTable();

        if (dither) {
            int index;
            for (int y = 0; y < height; y++) {
                boolean reverse = serpentine && (y & 1) == 1;
                int direction;
                if (reverse) {
                    index = y * width + width - 1;
                    direction = -1;
                } else {
                    index = y * width;
                    direction = 1;
                }
                for (int x = 0; x < width; x++) {
                    int rgb1 = inPixels[index];
                    int idx = quantizer.getIndexForColor(rgb1);
                    int rgb2 = table[idx];

                    outPixels[index] = (byte)(idx&0xff);

                    int a1 = (rgb1 >> 24) & 0xff;
                    int r1 = (rgb1 >> 16) & 0xff;
                    int g1 = (rgb1 >> 8) & 0xff;
                    int b1 = rgb1 & 0xff;

                    int a2 = (rgb2 >> 24) & 0xff;
                    int r2 = (rgb2 >> 16) & 0xff;
                    int g2 = (rgb2 >> 8) & 0xff;
                    int b2 = rgb2 & 0xff;

                    int ea = a1-a2;
                    int er = r1-r2;
                    int eg = g1-g2;
                    int eb = b1-b2;

                    for (int i = -1; i <= 1; i++) {
                        int iy = i+y;
                        if (0 <= iy && iy < height) {
                            for (int j = -1; j <= 1; j++) {
                                int jx = j+x;
                                if (0 <= jx && jx < width) {
                                    int w;
                                    if (reverse)
                                        w = FS_MATRIX[(i+1)*3-j+1];
                                    else
                                        w = FS_MATRIX[(i+1)*3+j+1];
                                    if (w != 0) {
                                        int k = reverse ? index - j : index + j;
                                        rgb1 = inPixels[k];

                                        a1 = (rgb1 >> 24) & 0xff;
                                        r1 = (rgb1 >> 16) & 0xff;
                                        g1 = (rgb1 >> 8) & 0xff;
                                        b1 = rgb1 & 0xff;
                                        a1 += ea * w/SUM;
                                        r1 += er * w/SUM;
                                        g1 += eg * w/SUM;
                                        b1 += eb * w/SUM;
                                        inPixels[k] = (clamp(a1) << 24 | clamp(r1) << 16) | (clamp(g1) << 8) | clamp(b1);
                                    }
                                }
                            }
                        }
                    }
                    index += direction;
                }
            }
        }

        // create palette
        HashMap<Integer,Integer> p = new HashMap<Integer,Integer>();

        for (int i = 0; i < count; i++) {
            int color;
            if (dither) {
                color = table[outPixels[i]&0xff];
            } else {
                color = table[quantizer.getIndexForColor(inPixels[i])];
            }
            int idx = p.size();
            Integer idxEx = p.get(color);
            if (idxEx == null) {
                p.put(color, idx);
            } else {
                idx = idxEx;
            }
            outPixels[i] = (byte)(idx);
        }

        Set <Integer>keys = p.keySet();
        int pal[] = new int[p.size()];
        for (int k : keys) {
            pal[p.get(k)] = k;
        }

        return pal;
    }
} public static void setByte(byte[] buffer, int index, int val) {
    buffer[index] = (byte)(val);
} public static void setDWord(byte[] buffer, int index, int val) {
    buffer[index]     = (byte)(val >> 24);
    buffer[index + 1] = (byte)(val >> 16);
    buffer[index + 2] = (byte)(val >> 8);
    buffer[index + 3] = (byte)(val);
} public static void setWord(byte[] buffer, int index, int val) {
    buffer[index]     = (byte)(val >> 8);
    buffer[index + 1] = (byte)(val);
}
import bdsup2sub.bitmap.Bitmap;
import bdsup2sub.bitmap.Palette;
import bdsup2sub.core.Configuration;
import bdsup2sub.core.Framerate;
import bdsup2sub.core.Logger;
import bdsup2sub.supstream.SubPicture;
import bdsup2sub.tools.QuantizeFilter;
import java.util.ArrayList;
import java.util.Iterator;
import static bdsup2sub.utils.ByteUtils.setByte;
import static bdsup2sub.utils.ByteUtils.setDWord;
import static bdsup2sub.utils.ByteUtils.setWord;
package bdsup2sub.supstream.bd;




public final class SupBDWriter {

    private static final Configuration configuration = Configuration.getInstance();
    private static final Logger logger = Logger.getInstance();

    private static final byte PACKET_HEADER[] = {
            0x50, 0x47,             // 0:  "PG"
            0x00, 0x00, 0x00, 0x00, // 2:  PTS - presentation time stamp
            0x00, 0x00, 0x00, 0x00, // 6:  DTS - decoding time stamp
            0x00,                   // 10: segment_type
            0x00, 0x00,             // 11: segment_length (bytes following till next PG)
    };

    private static final byte HEADER_PCS_START[] = {
            0x00, 0x00, 0x00, 0x00, // 0: video_width, video_height
            0x10,                   // 4: hi nibble: frame_rate (0x10=24p), lo nibble: reserved
            0x00, 0x00,             // 5: composition_number (increased by start and end header)
            (byte)0x80,             // 7: composition_state (0x80: epoch start)
            0x00,                   // 8: palette_update_flag (0x80), 7bit reserved
            0x00,                   // 9: palette_id_ref (0..7)
            0x01,                   // 10: number_of_composition_objects (0..2)
            0x00, 0x00,             // 11: 16bit object_id_ref
            0x00,                   // 13: window_id_ref (0..1)
            0x00,                   // 14: object_cropped_flag: 0x80, forced_on_flag = 0x040, 6bit reserved
            0x00, 0x00, 0x00, 0x00  // 15: composition_object_horizontal_position, composition_object_vertical_position
    };

    private static final byte HEADER_PCS_END[] = {
            0x00, 0x00, 0x00, 0x00, // 0: video_width, video_height
            0x10,                   // 4: hi nibble: frame_rate (0x10=24p), lo nibble: reserved
            0x00, 0x00,             // 5: composition_number (increased by start and end header)
            0x00,                   // 7: composition_state (0x00: normal)
            0x00,                   // 8: palette_update_flag (0x80), 7bit reserved
            0x00,                   // 9: palette_id_ref (0..7)
            0x00,                   // 10: number_of_composition_objects (0..2)
    };


    private static final byte HEADER_ODS_FIRST[] = {
            0x00, 0x00,             // 0: object_id
            0x00,                   // 2: object_version_number
            (byte)0xC0,             // 3: first_in_sequence (0x80), last_in_sequence (0x40), 6bits reserved
            0x00, 0x00, 0x00,       // 4: object_data_length - full RLE buffer length (including 4 bytes size info)
            0x00, 0x00, 0x00, 0x00, // 7: object_width, object_height
    };

    private static final byte HEADER_ODS_NEXT[] = {
            0x00, 0x00,             // 0: object_id
            0x00,                   // 2: object_version_number
            (byte)0x40,             // 3: first_in_sequence (0x80), last_in_sequence (0x40), 6bits reserved
    };

    private static final byte HEADER_WDS[] = {
            0x01,                   // 0 : number of windows (currently assumed 1, 0..2 is legal)
            0x00,                   // 1 : window id (0..1)
            0x00, 0x00, 0x00, 0x00, // 2 : x-ofs, y-ofs
            0x00, 0x00, 0x00, 0x00  // 6 : width, height
    };

    private SupBDWriter() {
    }

    /**
     * Create the binary stream representation of one caption
     * @param pic SubPicture object containing caption info
     * @param bm bitmap
     * @param pal palette
     * @return byte buffer containing the binary stream representation of one caption
     */
    public static byte[] createSupFrame(SubPicture pic, Bitmap bm, Palette pal) {