/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */

package net.ranides.assira.text.format;

import java.awt.Color;
import java.awt.Font;
import java.io.Serializable;
import java.util.Iterator;
import net.ranides.assira.math.Bitwise;

/**
 * Klasa reprezentująca ciąg znaków razem z przypisanymi do nich atrybutami 
 * (kolor tekstu, tła, czcionka, styl)
 * @author ranides
 */
public abstract class AnsiCharSequence implements CharSequence, Serializable {
    
    private static final long serialVersionUID = 4L;
    
    private int hash;
    
    /**
     * Odczytuje z sekwencji znak o podanym indeksie. Zwraca wynik identyczny jak
     * {@link #charAt}. Metoda istniejąca w celu uzyskania pełnej kompatybilności
     * sekwencji znaków z konwencją JavaBeans.
     * @param index
     * @return
     */
    public abstract char getCharAt(int index);
    
    /**
     * Zwraca numer koloru tekstu przypisany do znaku o podanym indeksie. 
     * Numer może być zamieniony na graficzną reprezentację za pomocą {@link AnsiColorScheme}.
     * @see #getComputedForeAt 
     * @param index
     * @return
     */
    public abstract int getForeAt(int index);
    
    /**
     * Zwraca numer koloru tła przypisany do znaku o podanym indeksie. 
     * Numer może być zamieniony na graficzną reprezentację za pomocą {@link AnsiColorScheme}.
     * @see #getComputedBackAt
     * @param index
     * @return
     */
    public abstract int getBackAt(int index);
    
    /**
     * Zwraca atrybuty przypisane do znaku o podanym indeksie. 
     * @param index
     * @return
     */
    public abstract int getFlagsAt(int index);
    
    /**
     * Zwraca numer czcionki przypisany do znaku o podanym indeksie. 
     * Numer może być zamieniony na graficzną reprezentację za pomocą {@link AnsiFontScheme}.
     * @see #getComputedFontAt
     * @param index
     * @return
     */
    public abstract int getFontAt(int index);
    
    /**
     * Zwraca wszystkie właściwości przypisane do znaku o podanym indeksie
     * (kolor, tło, czcionka, styl).
     * @param index
     * @return
     */
    public final AnsiAttr getAttrAt(int index) {
        return new AttrView(index);
    }
    
    /**
     * Metoda opcjonalna: zwraca kolor tekstu przypisany do znaku o podanym indeksie.
     * Ogólnie metoda zwraca rozsądny wynik, jeśli obiekt został powiązany 
     * z odpowiednim schematem kolorów
     * ({@link AnsiColorScheme}.
     * @param index
     * @return
     */
    public abstract Color getComputedForeAt(int index);
    
    /**
     * Metoda opcjonalna: zwraca kolor tła przypisany do znaku o podanym indeksie.
     * Ogólnie metoda zwraca rozsądny wynik, jeśli obiekt został powiązany 
     * z odpowiednim schematem kolorów
     * ({@link AnsiColorScheme}.
     * @param index
     * @return
     */
    public abstract Color getComputedBackAt(int index);    

    /**
     * Metoda opcjonalna: zwraca numer czcionki przypisany do znaku o podanym indeksie.
     * Ogólnie metoda zwraca rozsądny wynik, jeśli obiekt został powiązany 
     * z odpowiednim schematem czcionek
     * ({@link AnsiFontScheme}.
     * @param index
     * @return
     */
    public abstract Font getComputedFontAt(int index);
    
    /**
     * Zwraca schemat kolorów skojarzony z sekwencją znaków. Schemat powiązany
     * z sekwencją odpowiada za prawidłowe działanie metod {@link #getComputedFore},
     * {@link #getComputedBack}, {@link #getComputedFont} oraz pochodnych.
     * @return
     */
    public abstract AnsiScheme getScheme();
    
    public abstract void setScheme(AnsiScheme scheme);
    
    public abstract void setCharAt(int index, char value);
    
    public void replace(int index, CharSequence value) {
        for(int i=0, n = value.length(); i<n; i++) {
            setCharAt(index+i, value.charAt(i));
        }
    }
    
    public void setBackAt(int index, int color) {
        setBack(index, index+1, color);
    }
    
    public void setForeAt(int index, int color) {
        setFore(index, index+1, color);
    }
    
    public void setFontAt(int index, int font) {
        setFont(index, index+1, font);
    }
    
    public void setFlagsAt(int index, int flags) {
        setFlags(index, index+1, flags);
    }
    
    public void setAttrAt(int index, AnsiAttr attr) {
        setBackAt(index, attr.getBack());
        setForeAt(index, attr.getFore());
        setFontAt(index, attr.getFont());
        setFlagsAt(index, attr.getFlags());
    }

    public abstract void setBack(int from, int to, int attr);
    
    public abstract void setFore(int from, int to, int attr);
    
    public abstract void setFont(int from, int to, int font);
    
    public abstract void setFlags(int from, int to, int flags);
    
    public final void setAttr(int from, int to, AnsiAttr attr) {
        setBack(from, to, attr.getBack());
        setFore(from, to, attr.getFore());
        setFont(from, to, attr.getFont());
        setFlags(from, to, attr.getFlags());
    }
    
    /**
     * Rozbija tekst na fragmenty według kryterium {@link AnsiAttr#CMP_ALL}
     * @return
     */
    public final Iterable<AnsiRegion> regions() {
        return regions(AnsiAttr.CMP_ALL);
    }
    
    /**
     * <p>
     * Rozbija tekst na fragmenty zawierające znaki zgrupowane według kryteriów 
     * podanych w opcjach. Opcje są zbiorem flag z klasy {@link AnsiAttr}.
     * </p><p>
     * Rozdziela tekst na fragmenty o jednym kolorze tekstu i tła:<br/>
     * {@code text.split(AnsiAttr.CMP_FORE | AnsiAttr.CMP_BACK)}
     * </p><p>
     * Rozdziela tekst na fragmenty o tym samym stylu pogrubienia:<br/>
     * {@code text.split(AnsiAttr.CMP_FLAGS | AnsiAttr.BOLD)}
     * </p>
     * @param options
     * @return
     */
    public Iterable<AnsiRegion> regions(final int options) {
        return new Iterable<AnsiRegion>() {
            @Override
            public Iterator<AnsiRegion> iterator() {
                return new RegionIterator(options);
            }
        };
    }
    
    @Override
    public final char charAt(int index) {
        return getCharAt(index);
    }
    
    @Override
    public AnsiCharSequence subSequence(int begin, int end) {
        return new Fragment(this, begin, end);
    }
    
    @Override
    public int hashCode() {
        int h = hash;
        int n = length();
        if (h == 0 && n>0) {
            for (int i = 0; i < n; i++) {
                h = 31 * h + charAt(i) + 37 * getBackAt(i) + 53 * getForeAt(i);
            }
            hash = h;
        }
        return h;
    }

    @Override
    public boolean equals(Object object) {
        if(object instanceof AnsiCharSequence) {
            return equals((AnsiCharSequence)object, AnsiAttr.CMP_PUBLIC);
        }
        return false;
    }
    
    public boolean equals(AnsiCharSequence value, int flags) {
        if( this == value ) { // NOPMD
            return true;
        }
        if( value.length() != length() ) {
            return false;
        }
        for(int i=0, n=value.length(); i<n; i++) {
            if (Bitwise.has(flags, AnsiAttr.CMP_CHAR) && value.charAt(i) != charAt(i)) {
                return false;
            }
            if( !value.getAttrAt(i).equals(getAttrAt(i), flags) ) {
                return false;
            }
        }
        return true;
    }

    @Override
    public final String toString() {
        return new StringBuilder(length()).append(this).toString();
    }
    
    
    private class RegionIterator implements Iterator<AnsiRegion> {
        
        private final int options;
        private int index = 0;

        public RegionIterator(int options) {
            this.options = options;
        }

        @Override
        public boolean hasNext() {
            return index < AnsiCharSequence.this.length();
        }

        @Override
        @SuppressWarnings("PMD.CyclomaticComplexity")
        public AnsiRegion next() {
            final int start = index;
            final AttrView attr = new AttrView(start);
            final boolean sfc = Bitwise.has(options, AnsiAttr.CMP_FORE);
            final boolean sfb = Bitwise.has(options, AnsiAttr.CMP_BACK);
            final boolean sfs = Bitwise.has(options, AnsiAttr.CMP_FLAGS);
            final boolean sff = Bitwise.has(options, AnsiAttr.CMP_FONT);
            
            index++;
            int max = AnsiCharSequence.this.length();
            while(index<max) {
                if( sfc && getForeAt(index)!=attr.getFore()) { break; }
                if( sfb && getBackAt(index)!=attr.getBack() ) { break;  }
                if( sfs && (getFlagsAt(index) & options)!=(attr.getFlags()&options)) { break; }
                if( sff && getFontAt(index)!=attr.getFont()) { break; }
                index++;
            }
            
            return new AnsiRegion(attr, subSequence(start, index));
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("Strings are immutable.");
        }
    }
    
    private class AttrView extends AnsiAttr {
        
        private static final long serialVersionUID = 3L;
        
        private final int index;

        /**
         * Tworzy obiekt reprezentujący atrybuty znaku o podanym indeksie, zapisanego
         * w podanej sekwencji. Modyfikacja powiązanej sekwencji natychmiast jest
         * widoczna w obiekcie.
         * @param sequence
         * @param index
         */
        public AttrView(int index) {
            this.index = index;
        }

        @Override
        public Color getComputedBack() {
            return getComputedBackAt(index);
        }

        @Override
        public Font getComputedFont() {
            return getComputedFontAt(index);
        }

        @Override
        public Color getComputedFore() {
            return getComputedForeAt(index);
        }

        @Override
        public int getFlags() {
            return getFlagsAt(index);
        }

        @Override
        public int getBack() {
            return getBackAt(index);
        }

        @Override
        public int getFont() {
            return getFontAt(index);
        }

        @Override
        public int getFore() {
            return getForeAt(index);
        }
        
    }
    
    private class Fragment extends AnsiCharSequence {

        private static final long serialVersionUID = 3L;

        protected final AnsiCharSequence source;
        protected final int left;
        protected final int right;

        public Fragment(AnsiCharSequence source, int left, int right) {
            this.source = source;
            this.left = left;
            this.right = right;
        }

        @Override
        public Color getComputedForeAt(int index) {
            return source.getComputedForeAt(left + index);
        }

        @Override
        public Color getComputedBackAt(int index) {
            return source.getComputedBackAt(left + index);
        }

        @Override
        public int getForeAt(int index) {
            return source.getForeAt(left + index);
        }

        @Override
        public int getBackAt(int index) {
            return source.getBackAt(left + index);
        }

        @Override
        public int getFlagsAt(int index) {
            return source.getFlagsAt(left + index);
        }

        @Override
        public Font getComputedFontAt(int index) {
            return source.getComputedFontAt(left + index);
        }

        @Override
        public int getFontAt(int index) {
            return source.getFontAt(left + index);
        }

        @Override
        public AnsiCharSequence subSequence(int begin, int end) {
            return new Fragment(source, left+begin, left+end);
        }

        @Override
        public int length() {
            return right - left;
        }

        @Override
        public char getCharAt(int index) {
            return source.charAt(left + index);
        }

        @Override
        public AnsiScheme getScheme() {
            return source.getScheme();
        }

        @Override
        public void setScheme(AnsiScheme scheme) {
            source.setScheme(scheme);
        }

        @Override
        public void setCharAt(int index, char value) {
            source.setCharAt(left + index, value);
        }
        
        @Override
        public void setBackAt(int index, int color) {
            source.setBackAt(left+index, color);
        }

        @Override
        public void setForeAt(int index, int color) {
            source.setForeAt(left+index, color);
        }

        @Override
        public void setFontAt(int index, int font) {
            source.setFontAt(left+index, font);
        }

        @Override
        public void setFlagsAt(int index, int flags) {
            source.setFlagsAt(left+index, flags);
        }

        @Override
        public void setAttrAt(int index, AnsiAttr attr) {
            source.setAttrAt(left+index, attr);
        }

        @Override
        public void setBack(int from, int to, int attr) {
            source.setBack(left + from, left + to, attr);
        }

        @Override
        public void setFore(int from, int to, int attr) {
            source.setFore(left + from, left + to, attr);
        }

        @Override
        public void setFont(int from, int to, int font) {
            source.setFont(left + from, left + to, font);
        }

        @Override
        public void setFlags(int from, int to, int flags) {
            source.setFlags(left + from, left + to, flags);
        }
        
        @Override
        public boolean equals(Object object) {
            if(this == object) {
                    return true;
                }
            if (object instanceof Fragment) {
                final Fragment other = (Fragment)object;
                if( source == other.source )  {
                    return (left == other.left) && (right == other.right);
                }
            }
            return super.equals(object);
        }

        @Override
        public int hashCode() {
            return super.hashCode();
        }
        
        

    }
}
