/**
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 *
 * Copyright (C)
 * <a href="http://www.inab.org">Spanish National Institute of Bioinformatics (INB)</a>
 * <a href="http://www.bsc.es">Barcelona Supercomputing Center (BSC)</a>
 * <a href="http://inb.bsc.es">Computational Node 6</a>
 */

package org.inb.biomoby.client.gui;

import java.awt.Font;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.LineMetrics;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;

/**
 * @author Dmitry Repchevsky
 */

public class SequenceTransformerBean implements ISequenceTransformer
{
    private int width;
    private int height;
    
    private int length;
    
    private int position;

    private int firstFixedCharPosition;
    private int lastFixedCharPosition;

    private int firstVisibleChar;
    private int lastVisibleChar;

    private int charWidth; // preferred character width...

    private int segmentStart; // X coordinate where the 'position' charater is going to be painted
    private int segmentEnd; // X coordinate where non-scaled string is finished...
    
    private boolean isRefreshNeeded;
    private boolean isFontRefreshNeeded;
    
    private Font font;
    private FontRenderContext frc;
    private LineMetrics lm;

    public SequenceTransformerBean()
    {
        frc = new FontRenderContext(null, true, false);
        
        isRefreshNeeded = true;
        isFontRefreshNeeded = true;
        
//        firstFixedCharPosition = Integer.MAX_VALUE; // trick to enforce recalculation...
    }

    @Override
    public void setWidth(int width)
    {
        if (this.width != width)
        {
            this.width = width;
            
            isRefreshNeeded = true;
        }
    }
    
    @Override
    public void setHeight(int height)
    {
        if (this.height != height)
        {
            this.height = height;
            
            isRefreshNeeded = true;
            isFontRefreshNeeded = true;
        }
    }

    @Override
    public int getLength()
    {
        return length;
    }
    
    @Override
    public void setLength(int length)
    {
        if (this.length != length)
        {
            this.length = length;
            
            isRefreshNeeded = true;
        }
    }

    @Override
    public int getPosition()
    {
        return position;
    }
    
    @Override
    public void setPosition(int position)
    {
        if (this.position != position)
        {
            this.position = position;
            
//            if (position < firstFixedCharPosition || position > lastFixedCharPosition)
            {
                isRefreshNeeded = true;
            }
        }
    }

    @Override
    public int getFirstVisibleChar()
    {
        refresh();

        return firstVisibleChar;
    }
    
    @Override
    public int getLastVisibleChar()
    {
        refresh();

        return lastVisibleChar;
    }

    @Override
    public int getFirstFixedCharPosition()
    {
        refresh();
        
        return firstFixedCharPosition;
    }

    @Override
    public int getLastFixedCharPosition()
    {
        refresh();
        
        return lastFixedCharPosition;
    }
    
    
    @Override
    public double getXCharPosition(int position)
    {
        refresh();

        if (firstFixedCharPosition > position)
        { // first segment
            if (position < firstVisibleChar)
            {
                return 0;
            }
            return segmentStart - getXCharPosition(firstFixedCharPosition - position, firstFixedCharPosition - firstVisibleChar, charWidth);
        }
        else if (lastFixedCharPosition < position)
        { // last segment
            if (position > lastVisibleChar)
            {
                return width;
            }
            return segmentEnd + getXCharPosition(position - lastFixedCharPosition - 1, lastVisibleChar - lastFixedCharPosition - 1, charWidth);
        }
        else
        { // central segment
            return segmentStart + charWidth * (position - firstFixedCharPosition);
        }
    }

    @Override
    public double getXCharWidth(int position)
    {
        refresh();

        if (firstFixedCharPosition > position)
        {
            if (position < firstVisibleChar)
            {
                return 0;
            }
            return charWidth * getXScale(firstFixedCharPosition - position - 1, firstFixedCharPosition - firstVisibleChar);
        }
        else if (lastFixedCharPosition < position)
        {
            if (position > lastVisibleChar)
            {
                return 0;
            }
            return charWidth * getXScale(position - lastFixedCharPosition, lastVisibleChar - lastFixedCharPosition);
        }
        else
        {
            return charWidth;
        }
    }
    
    @Override
    public int getXFixedSegmentStart()
    {
        return segmentStart;
    }
    
    @Override
    public int getXFixedSegmentEnd()
    {
        return segmentEnd;
    }
    
    @Override
    public GlyphVector getGlyphVector(CharSequence sequence)
    {
        refresh();

//        System.out.println("segmentStart = " + segmentStart + " segmentEnd = " + segmentEnd);
//        System.out.println("firstVisibleChar = " + firstVisibleChar + " lastVisibleChar = " + lastVisibleChar);
//        System.out.println("firstFixedCharPosition = " + firstFixedCharPosition + " lastFixedCharPosition = " + lastFixedCharPosition);
//        System.out.println(sequence.substring(firstVisibleChar, lastVisibleChar));

        CharSequence visible = sequence.subSequence(firstVisibleChar, Math.min(lastVisibleChar + 1, length));

        GlyphVector gv = font.createGlyphVector(frc, visible.toString());

        final float descent = -lm.getDescent()/2;
        for (int i = 0, n = visible.length(), pos = firstVisibleChar; i < n; i++, pos++)
        {
            final double x = getXCharPosition(pos);
            final double chWidth = getXCharWidth(pos);
            final double xScale = chWidth/charWidth;
            
//            System.out.println("pos = " + pos + " x = " + x + " chWidth = " + chWidth + " xScale = " + xScale);
            AffineTransform at = AffineTransform.getScaleInstance(xScale, 1);

            gv.setGlyphTransform(i, at);
            gv.setGlyphPosition(i, new Point2D.Double(x, descent));
        }
        
        return gv;
    }
    
    private void refresh()
    {
        if (isFontRefreshNeeded)
        {
            fontRefresh();
            isFontRefreshNeeded = false;
        }
        
        if (isRefreshNeeded)
        {
            final int stringWidth = charWidth * length;
            final int freeSpace = width - stringWidth;

            if (freeSpace >= 0)
            {
                segmentStart = freeSpace/2;
                segmentEnd = width - segmentStart;

                firstVisibleChar = 0;
                firstFixedCharPosition = firstVisibleChar;

                lastVisibleChar = Math.max(0, length - 1);
                lastFixedCharPosition = lastVisibleChar;
            }
            else
            {
                final float ratio = width/(float)stringWidth; // how much our visible area is less than the string

                int window = width * 3/5; // always reserve 3/5 of the visible area to show the part of the string
                int margen = (int) (2 * width * ratio)/5; // variable part to extend the 'static' visible frame

                window += margen; // the 'static' part of the string is 1/3 + some 'x'

                // calculate exact number of characters to show without scaling
                final int chars2show = window/charWidth;//Math.min(length - firstFixedCharPosition, window/charWidth);

                if (position > lastFixedCharPosition)
                {
                    lastFixedCharPosition = position;
                    firstFixedCharPosition = lastFixedCharPosition - chars2show + 1;
                }
                else if (position < firstFixedCharPosition)
                {
                    firstFixedCharPosition = position;
                    lastFixedCharPosition = firstFixedCharPosition + chars2show - 1;
                }
                else
                {
                    int oldVis = lastFixedCharPosition - firstFixedCharPosition;
                    firstFixedCharPosition = Math.min(position, firstFixedCharPosition + (chars2show - oldVis)/2);
                    lastFixedCharPosition = Math.min(length, firstFixedCharPosition + chars2show - 1);
                }

                window = chars2show * charWidth; // final static window width;

                float shift = (float)firstFixedCharPosition/(length - chars2show);

                segmentStart = (int)((width - window) * shift);
                segmentEnd = segmentStart + window;

                final int leadChars = getVisibleChars(segmentStart, charWidth); // lead visible characters
                firstVisibleChar = Math.max(0, firstFixedCharPosition - leadChars);

                final int tailChars = getVisibleChars(width - segmentEnd, charWidth); // tail visible characters
                lastVisibleChar = Math.min(length, firstFixedCharPosition + chars2show + tailChars);
            }
            
            isRefreshNeeded = false;
        }
    }

    private double getXCharPosition(int position, int n, int charWidth)
    {
        double x = 0;
        for (int i = 0; i < position; i++)
        {
            x += getXScale(i, n) * charWidth;
        }
        
        return x;
    }
    
    private int getVisibleChars(int wFrame, int charWidth)
    {
        double w;
        int chars = wFrame/charWidth - 1;
        do
        {
            chars++;
            w = 0;
            for (int i = 0; i < chars; i++)
            {
                w += getXScale(i, chars) * charWidth;
            }
        }
        while(w < wFrame);
        
        return chars;
    }

    private static double getXScale(int x, int n)
    {
        double w = Math.cos(Math.PI/4 + Math.PI * x/(4 * n));
       // double w = Math.cos(Math.PI * x/(2 * n));

        return w;
    }
    
    private void fontRefresh()
    {
        font = new Font("Monospaced", Font.PLAIN, (int)(height * 0.7));
        lm = font.getLineMetrics("A", frc);
        Rectangle2D rec = font.getStringBounds("A", frc);
        
        charWidth = (int)rec.getWidth();
    }
}
