/*
 * Copyright (c) 2001-2005 Frederic Banino. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of Frederic Banino nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.frban.graphics.hatch;

import java.lang.ref.*;

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;

/**
 * un contexte simplifié de hachures, qui utilise un buffer offscreen static,
 * afin d'optimiser au mieux la mémoire. Noter aussi que les transformations
 * du contexte graphique ne sont pas prises en compte, ce qui rend les hachures
 * invariantes à des opérations de rotation ou d'échelle. Cet effet n'est pas
 * focément souhaitable, mais c'est celui que je voulais donner.
 * @version 1.0
 * @author FB
 * @see HatchedPaint
 */
public class HatchedPaintContext implements PaintContext {
    /** modèle de couleur utilisé pour rendre les hachures */
    private static ColorModel cachedModel;

    /** référence faible associée au buffer. Avec ce système, on est sûr que
     * de garbage collector effacera le cache dès qu'on en a plus besoin.
     */
    private static WeakReference cached;

    /**
     * fabrication d'un buffer image pour que les hachures soient d'abord
     * rendues en offscreen. Si le cache existe déjà et que la taille d'image
     * n'a pas bougé, on réutilise le même buffer.
     * @param cm  modèle couleur du buffer
     * @param w   largeur du buffer
     * @param h   hauteur du buffer
     * @return    buffer image prêt à l'emploi
     */
    private static synchronized Raster getCachedRaster(ColorModel cm, int w, int h) {
        if (cm == cachedModel) {
            if (cached != null) {
                Raster ras = (Raster) cached.get();
                if (ras != null && ras.getWidth() >= w && ras.getHeight() >= h) {
                    cached = null;
                    return ras;
                }
            }
        }
        return cm.createCompatibleWritableRaster(w, h);
    }

    /**
     * Intègre un buffer en cache, pour rendre les hachures offscreen. Le
     * buffer est stocké dans une WeakReference, de sorte qu'il soit libéré
     * prioritairement par le garbage collector quand on n'en aura plus besoin.
     * Ce procédé assure une gestion mémoire propre.
     * @param cm   modèle couleur du buffer
     * @param ras  buffer à mettre en cache.
     */
    private static synchronized void putCachedRaster(ColorModel cm, Raster ras) {
        if (cached != null) {
            Raster cras = (Raster) cached.get();
            if (cras != null) {
                int cw = cras.getWidth();
                int ch = cras.getHeight();
                int iw = ras.getWidth();
                int ih = ras.getHeight();
                if (cw >= iw && ch >= ih) return;
                if (cw * ch >= iw * ih) return;
            }
        }
        cachedModel = cm;
        cached = new WeakReference(ras);
    }

    //--------------------------------------------------------------------------

    /**
     * modèle par défaut ColorModel.getRBGdefault, capable de gérer la
     * transparence (ARGB);
     */
    private static ColorModel argbmodel = ColorModel.getRGBdefault();

    /** couleur des hachures */
    private int[] colors;
    /** espacement entre les hachures */
    private float space;
    /** angle des hachures */
    private float angle;
    /** épaisseur de trait */
    private float lineWidth;

    /** paramètre de formation de couleurs intermédiaires d'antialiasing */
    private int aliasMax = 25;

    /** transformation de positionnement du gradient */
    private AffineTransform gat;

    /** raster conservé d'un appel à un autre */
    private Raster saved;

    /**
     * fabrique la table des couleurs d'antialiasing. Cette table comprend
     * aliaxMax éléments. La couleur c est situé au milieu, puis des nuances
     * de plus en plus transparentes sont crées pour aller vers les extrémités.
     * @param c   couleur des hachures.
     */
    protected void createAliasedColorTable(Color c) {
        int cc = c.getRGB();
        float a = (0xFF & (cc>>24))/255f; //alpha
        cc=0xFFFFFF & cc; //ôte la transparence éventuelle

        colors = new int[aliasMax];
            //la table est symétrique avec la couleur opaque au centre
            //et des nuances de + en + transparentes à g et à d.
        for(int i=0; i<aliasMax/2; i++) {
            int alpha = (int)(i/(aliasMax/2f-1f)*a*255.0f);
            alpha <<= 24;
            colors[i] = alpha | cc;
            colors[colors.length-i-1] = alpha | cc;
        }
            //couleur opaque à ajouter au centre du tableau quand celui-ci
            //est impair.
        if (aliasMax%2!=0) colors[aliasMax/2]= 0xFF000000 | cc;
    }

    /** nouveau contexte */
    public HatchedPaintContext(Color c, float space, float angle, float lineWidth) {
        this.space=space;
        this.angle=angle;
        this.lineWidth = lineWidth;
        createAliasedColorTable(c);
        gat = new AffineTransform();
            //décalage de pi/2 pour que l'angle 0 soit horizontal
        gat.rotate(Math.PI/2+angle);
    }


    /** efface les références */
    public void dispose() {
        if (saved != null) {
            putCachedRaster(argbmodel, saved);
            saved = null;
        }
    }

    /**
     * renvoie toujours un modèle ARGB
     * @see #argbmodel
     */
    public ColorModel getColorModel() { return argbmodel; }

    /**
     * dessine les hachures dans le buffer offscreen.
     * @return buffer où ont été dessinées les hachures.
     */
    public Raster getRaster(int x, int y, int w, int h) {
        float xref=0;
        float distance = space;

        //création du buffer s'il n'existe pas encore
        Raster rast = saved;
        if (rast == null || rast.getWidth() < w || rast.getHeight() < h) {
            rast = getCachedRaster(argbmodel, w, h);
            saved = rast;
        }
        DataBufferInt buffer = (DataBufferInt) rast.getDataBuffer();
        int off = buffer.getOffset();
        int adjust = rast.getWidth()-w;
        int[] pixels = buffer.getData();


        Point2D.Double p1=new Point2D.Double();
        Point2D.Double p2=new Point2D.Double();
        double r=0;

        double line;
        for(int yy=0; yy<h; yy++) {
            for(int xx=0; xx<w; xx++) {
                p1.x = x+xx;
                p1.y = y+yy;
                gat.transform(p1,p2);
                r = (p2.x-xref)/distance;
                r = r-Math.floor(r);

                line = Math.abs(r*distance);
                    //dessine si on se trouve proche du trait
                pixels[off++] = line<lineWidth ? colors[(int)(line/lineWidth*aliasMax)] : 0;
            }
            off += adjust;
        }

        return rast;
    }

    /** valeur de l'angle */
    public float getAngle() {
        return angle;
    }
}