package org.orbeon.saxon.codenorm;

import org.orbeon.saxon.charcode.UTF16;
import org.orbeon.saxon.trans.XPathException;
import org.orbeon.saxon.value.Whitespace;

/**
 * Implements Unicode Normalization Forms C, D, KC, KD.
 * Copyright (c) 1991-2005 Unicode, Inc.
 * For terms of use, see http://www.unicode.org/terms_of_use.html
 * For documentation, see UAX#15.<br>
 * The Unicode Consortium makes no expressed or implied warranty of any
 * kind, and assumes no liability for errors or omissions.
 * No liability is assumed for incidental and consequential damages
 * in connection with or arising out of the use of the information here.
 * @author Mark Davis
 * Updates for supplementary code points: Vladimir Weinstein & Markus Scherer
 * Modified to remove dependency on ICU code: Michael Kay
 */

public class Normalizer {

    /**
     * Create a normalizer for a given form.
     * @param form the normalization form required: for example {@link Normalizer#C}, {@link Normalizer#D}
     */
    public Normalizer(byte form) {
        this.form = form;
        if (data == null) {
            data = UnicodeDataParser.build(); // load 1st time
        }
    }
    
    /**
     * Create a normalizer for a given form, expressed as a character string
     * @param formCS the normalization form required: for example "NFC" or "NFD"
     */ 
    
    public Normalizer(CharSequence formCS) throws XPathException {
        String formString = Whitespace.trim(formCS);
        byte fb;
        if (formString.length() == 0) {
            fb = NO_ACTION;
        } else if (formString.equalsIgnoreCase("NFC")) {
            fb = Normalizer.C;
        } else if (formString.equalsIgnoreCase("NFD")) {
            fb = Normalizer.D;
        } else if (formString.equalsIgnoreCase("NFKC")) {
            fb = Normalizer.KC;
        } else if (formString.equalsIgnoreCase("NFKD")) {
            fb = Normalizer.KD;
        } else {
            String msg = "Normalization form " + formString + " is not supported";
            throw new XPathException(msg, "FOCH0003");
        }
        form = fb;
        if (data == null) {
            data = UnicodeDataParser.build(); // load 1st time
        }        
    }

    /**
    * Masks for the form selector
    */
    static final byte
        COMPATIBILITY_MASK = 1,
        COMPOSITION_MASK = 2;

    /**
    * Normalization Form Selector
    */
    public static final byte
        D = 0 ,
        C = COMPOSITION_MASK,
        KD = COMPATIBILITY_MASK,
        KC = (byte)(COMPATIBILITY_MASK + COMPOSITION_MASK),
        NO_ACTION = 8;

    /**
     * Normalizes text according to the chosen form,
     * replacing contents of the target buffer.
     * @param   source      the original text, unnormalized
     * @param   target      the resulting normalized text
     * @return the modified target StringBuffer
    */
    private StringBuffer normalize(CharSequence source, StringBuffer target) {

        if (form == NO_ACTION || source.length() == 0) {
            return new StringBuffer(source.toString());
        }

        // First decompose the source into target,
        // then compose if the form requires.

        internalDecompose(source, target);
        if ((form & COMPOSITION_MASK) != 0) {
            internalCompose(target);
        }
        return target;
    }

    /**
    * Normalizes text according to the chosen form
    * @param   source      the original text, unnormalized
    * @return  target      the resulting normalized text
    */
    public CharSequence normalize(CharSequence source) {
        return normalize(source, new StringBuffer(source.length()+8));
    }

    // ======================================
    //                  PRIVATES
    // ======================================

    /**
     * The current form.
     */
    private byte form;

    /**
    * Decomposes text, either canonical or compatibility,
    * replacing contents of the target buffer.
//    * @param   form        the normalization form. If COMPATIBILITY_MASK
//    *                      bit is on in this byte, then selects the recursive
//    *                      compatibility decomposition, otherwise selects
//    *                      the recursive canonical decomposition.
    * @param   source      the original text, unnormalized
    * @param   target      the resulting normalized text
    */
    private void internalDecompose(CharSequence source, StringBuffer target) {
        StringBuffer buffer = new StringBuffer(8);
        boolean canonical = (form & COMPATIBILITY_MASK) == 0;
        int ch32;
        //for (int i = 0; i < source.length(); i += (ch32<65536 ? 1 : 2)) {
        for (int i = 0; i < source.length();) {
            buffer.setLength(0);
            //ch32 = UTF16.charAt(source, i);
            ch32 = source.charAt(i++);
            if (UTF16.isHighSurrogate(ch32)) {
                char low = source.charAt(i++);
                ch32 = UTF16.combinePair((char)ch32, low);
            }
            data.getRecursiveDecomposition(canonical, ch32, buffer);

            // add all of the characters in the decomposition.
            // (may be just the original character, if there was
            // no decomposition mapping)

            int ch;
            //for (int j = 0; j < buffer.length(); j += (ch<65536 ? 1 : 2)) {
            for (int j = 0; j < buffer.length();) {
                //ch = UTF16.charAt(buffer, j);
                ch = buffer.charAt(j++);
                if (UTF16.isHighSurrogate(ch)) {
                    char low = buffer.charAt(j++);
                    ch = UTF16.combinePair((char)ch, low);
                }
                int chClass = data.getCanonicalClass(ch);
                int k = target.length(); // insertion point
                if (chClass != 0) {

                    // bubble-sort combining marks as necessary

                    int ch2;
                    while (k > 0) {
                        int step = 1;
                        ch2 = target.charAt(k-1);
                        if (UTF16.isSurrogate(ch2)) {
                            step = 2;
                            char high = target.charAt(k-2);
                            ch2 = UTF16.combinePair(high, (char)ch2);
                        }
                        if (data.getCanonicalClass(ch2) <= chClass) break;
                        k -= step;
                    }
//                    for (; k > 0; k -= (ch2<65536 ? 1 : 2)) {
//                        ch2 = UTF16.charAt(target, k-1);
//                        if (data.getCanonicalClass(ch2) <= chClass) break;
//                    }
                }
                if (ch < 65536) {
                    target.insert(k, (char)ch);
                } else {
                    char[] chars = new char[]{UTF16.highSurrogate(ch), UTF16.lowSurrogate(ch)};
                    target.insert(k, chars);
                }
                //target.insert(k, UTF16.valueOf(ch));
            }
        }
    }

    /**
    * Composes text in place. Target must already
    * have been decomposed.
    * @param   target      input: decomposed text.
    *                      output: the resulting normalized text.
    */
    private void internalCompose(StringBuffer target) {

        int starterPos = 0;
        //int starterCh = UTF16.charAt(target,0);
        //int compPos = (starterCh<65536 ? 1 : 2); // length of last composition
        int starterCh = target.charAt(0);
        int compPos = 1;
        if (UTF16.isHighSurrogate(starterCh)) {
            starterCh = UTF16.combinePair((char)starterCh, target.charAt(1));
            compPos++;
        }
        int lastClass = data.getCanonicalClass(starterCh);
        if (lastClass != 0) lastClass = 256; // fix for strings staring with a combining mark
        int oldLen = target.length();

        // Loop on the decomposed characters, combining where possible

        int ch;
        //for (int decompPos = compPos; decompPos < target.length(); decompPos += (ch<65536 ? 1 : 2)) {
        for (int decompPos = compPos; decompPos < target.length();) {
            ch = target.charAt(decompPos++);
            if (UTF16.isHighSurrogate(ch)) {
                ch = UTF16.combinePair((char)ch, target.charAt(decompPos++));
            }
            //ch = UTF16.charAt(target, decompPos);
            int chClass = data.getCanonicalClass(ch);
            int composite = data.getPairwiseComposition(starterCh, ch);
            if (composite != NormalizerData.NOT_COMPOSITE && (lastClass < chClass || lastClass == 0)) {
                setCharAt(target, starterPos, composite);
                // we know that we will only be replacing non-supplementaries by non-supplementaries
                // so we don't have to adjust the decompPos
                starterCh = composite;
            } else {
                if (chClass == 0) {
                    starterPos = compPos;
                    starterCh  = ch;
                }
                lastClass = chClass;
                setCharAt(target, compPos, ch);
                if (target.length() != oldLen) { // MAY HAVE TO ADJUST!
                    decompPos += target.length() - oldLen;
                    oldLen = target.length();
                }
                compPos += (ch<65536 ? 1 : 2);
            }
        }
        target.setLength(compPos);
    }

    /**
     * Set the 32-bit character at a particular 16-bit offset in a string buffer,
     * replacing the previous character at that position, and taking account of the
     * fact that either, both, or neither of the characters might be a surrogate pair.
     * @param target the StringBuffer in which the data is to be inserted
     * @param offset the position at which the data is to be inserted
     * @param ch32 the character to be inserted, as a 32-bit Unicode codepoint
     */

    private static void setCharAt(StringBuffer target, int offset, int ch32) {
        if (ch32 < 65536) {
            if (UTF16.isHighSurrogate(target.charAt(offset))) {
                target.setCharAt(offset, (char)ch32);
                target.deleteCharAt(offset+1);
            } else {
                target.setCharAt(offset, (char)ch32);
            }
        } else {
            if (UTF16.isHighSurrogate(target.charAt(offset))) {
                target.setCharAt(offset, UTF16.highSurrogate(ch32));
                target.setCharAt(offset+1, UTF16.lowSurrogate(ch32));
            } else {
                target.setCharAt(offset, UTF16.highSurrogate(ch32));
                target.insert(offset+1, UTF16.lowSurrogate(ch32));
            }
        }
    }

    /**
    * Contains normalization data from the Unicode Character Database.
    */
    private static NormalizerData data = null;

    /**
    * Just accessible for testing.
     * @param ch a character
     * @return true if the character is an excluded character
    */
    boolean getExcluded (char ch) {
        return data.getExcluded(ch);
    }

    /**
    * Just accessible for testing.
     * @param ch a character
     * @return the raw decomposition mapping of the character
    */
    String getRawDecompositionMapping (char ch) {
        return data.getRawDecompositionMapping(ch);
    }
}