/*
 * @(#)StringCoding.java	1.8 01/12/03
 *
 * Copyright 2002 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package java.lang;

import java.io.CharConversionException;
import java.io.UnsupportedEncodingException;
import java.lang.ref.SoftReference;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnsupportedCharsetException;
import sun.io.ByteToCharConverter;
import sun.io.CharToByteConverter;
import sun.io.Converters;
import sun.nio.cs.HistoricallyNamedCharset;


/**
 * Utility class for string encoding and decoding.
 */

class StringCoding {

    private StringCoding() { }

    /* The cached coders for each thread
     */
    private static ThreadLocal decoder = new ThreadLocal();
    private static ThreadLocal encoder = new ThreadLocal();

    private static boolean warnUnsupportedCharset = true;

    private static Object deref(ThreadLocal tl) {
        SoftReference sr = (SoftReference)tl.get();
	if (sr == null)
	    return null;
	return sr.get();
    }

    private static void set(ThreadLocal tl, Object ob) {
	tl.set(new SoftReference(ob));
    }

    // Trim the given byte array to the given length
    //
    private static byte[] trim(byte[] ba, int len) {
	if (len == ba.length)
	    return ba;
	byte[] tba = new byte[len];
	System.arraycopy(ba, 0, tba, 0, len);
	return tba;
    }

    // Trim the given char array to the given length
    //
    private static char[] trim(char[] ca, int len) {
	if (len == ca.length)
	    return ca;
	char[] tca = new char[len];
	System.arraycopy(ca, 0, tca, 0, len);
	return tca;
    }

    private static Charset lookupCharset(String csn) {
	if (Charset.isSupported(csn)) {
	    try {
		return Charset.forName(csn);
	    } catch (UnsupportedCharsetException x) {
		throw new Error(x);
	    }
	}
	return null;
    }

    private static void warnUnsupportedCharset(String csn) {
	if (warnUnsupportedCharset) {
	    // Use System.err rather than the Logging API since it may not
	    // have been loaded.
	    System.err.println("WARNING: Default charset " + csn +
			       " not supported, using ISO-8859-1 instead");
	    warnUnsupportedCharset = false;
	}
    }


    // -- Decoding --

    // Encapsulates either a ByteToCharConverter or a CharsetDecoder
    //
    private static abstract class StringDecoder {
	private final String requestedCharsetName;
	protected StringDecoder(String requestedCharsetName) {
	    this.requestedCharsetName = requestedCharsetName;
	}
	final String requestedCharsetName() {
	    return requestedCharsetName;
	}
	abstract String charsetName();
	abstract char[] decode(byte[] ba, int off, int len);
    }

    // A string decoder based upon a ByteToCharConverter
    //
    private static class ConverterSD
	extends StringDecoder
    {
	private ByteToCharConverter btc;

	private ConverterSD(ByteToCharConverter btc, String rcn) {
	    super(rcn);
	    this.btc = btc;
	}

	String charsetName() {
	    return btc.getCharacterEncoding();
	}

	char[] decode(byte[] ba, int off, int len) {
	    int en = btc.getMaxCharsPerByte() * len;
	    char[] ca = new char[en];
	    if (len == 0)
		return ca;
	    btc.reset();
	    int n = 0;
	    try {
		n = btc.convert(ba, off, off + len, ca, 0, en);
		n += btc.flush(ca, btc.nextCharIndex(), en);
	    } catch (CharConversionException x) {
		// Yes, this is what we've always done
		n = btc.nextCharIndex();
	    }
	    return trim(ca, n);
	}

    }

    // A string decoder based upon a CharsetDecoder
    //
    private static class CharsetSD
	extends StringDecoder
    {
	private final Charset cs;
	private final CharsetDecoder cd;

	private CharsetSD(Charset cs, String rcn) {
	    super(rcn);
	    this.cs = cs;
	    this.cd = cs.newDecoder()
		.onMalformedInput(CodingErrorAction.REPLACE)
		.onUnmappableCharacter(CodingErrorAction.REPLACE);
	}

	String charsetName() {
	    if (cs instanceof HistoricallyNamedCharset)
		return ((HistoricallyNamedCharset)cs).historicalName();
	    return cs.name();
	}

	char[] decode(byte[] ba, int off, int len) {
	    int en = (int)(cd.maxCharsPerByte() * len);
	    char[] ca = new char[en];
	    if (len == 0)
		return ca;
	    cd.reset();
	    ByteBuffer bb = ByteBuffer.wrap(ba, off, len);
	    CharBuffer cb = CharBuffer.wrap(ca);
	    try {
		CoderResult cr = cd.decode(bb, cb, true);
		if (!cr.isUnderflow())
		    cr.throwException();
		cr = cd.flush(cb);
		if (!cr.isUnderflow())
		    cr.throwException();
	    } catch (CharacterCodingException x) {
		// Substitution is always enabled,
		// so this shouldn't happen
		throw new Error(x);
	    }
	    return trim(ca, cb.position());
	}

    }

    static char[] decode(String charsetName, byte[] ba, int off, int len)
	throws UnsupportedEncodingException
    {
	StringDecoder sd = (StringDecoder)deref(decoder);
	String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
	if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
			      || csn.equals(sd.charsetName()))) {
	    sd = null;
	    try {
		Charset cs = lookupCharset(csn);
		if (cs != null)
		    sd = new CharsetSD(cs, csn);
		else
		    sd = null;
	    } catch (IllegalCharsetNameException x) {
		// FALL THROUGH to ByteToCharConverter, for compatibility
	    }
	    if (sd == null)
		sd = new ConverterSD(ByteToCharConverter.getConverter(csn),
				     csn);
	    set(decoder, sd);
	}
	return sd.decode(ba, off, len);
    }

    static char[] decode(byte[] ba, int off, int len) {
	String csn = Converters.getDefaultEncodingName();
	try {
	    return decode(csn, ba, off, len);
	} catch (UnsupportedEncodingException x) {
	    warnUnsupportedCharset(csn);
	}
	try {
	    return decode("ISO-8859-1", ba, off, len);
	} catch (UnsupportedEncodingException x) {
	    throw new Error("ISO-8859-1 charset not available", x);
	}
    }




    // -- Encoding --

    // Encapsulates either a CharToByteConverter or a CharsetEncoder
    //
    private static abstract class StringEncoder {
	private final String requestedCharsetName;
	protected StringEncoder(String requestedCharsetName) {
	    this.requestedCharsetName = requestedCharsetName;
	}
	final String requestedCharsetName() {
	    return requestedCharsetName;
	}
	abstract String charsetName();
	abstract byte[] encode(char[] cs, int off, int len);
    }

    // A string encoder based upon a CharToByteConverter
    //
    private static class ConverterSE
	extends StringEncoder
    {
	private CharToByteConverter ctb;

	private ConverterSE(CharToByteConverter ctb, String rcn) {
	    super(rcn);
	    this.ctb = ctb;
	}

	String charsetName() {
	    return ctb.getCharacterEncoding();
	}

	byte[] encode(char[] ca, int off, int len) {
	    int en = ctb.getMaxBytesPerChar() * len;
	    byte[] ba = new byte[en];
	    if (len == 0)
		return ba;

	    ctb.reset();
	    int n;
	    try {
		n = ctb.convertAny(ca, off, (off + len),
				   ba, 0, en);
		n += ctb.flushAny(ba, ctb.nextByteIndex(), en);
	    } catch (CharConversionException x) {
		throw new Error("Converter malfunction: " +
				ctb.getClass().getName(),
				x);
	    }
	    return trim(ba, n);
	}

    }

    // A string encoder based upon a CharsetEncoder
    //
    private static class CharsetSE
	extends StringEncoder
    {
	private Charset cs;
	private CharsetEncoder ce;

	private CharsetSE(Charset cs, String rcn) {
	    super(rcn);
	    this.cs = cs;
	    this.ce = cs.newEncoder()
		.onMalformedInput(CodingErrorAction.REPLACE)
		.onUnmappableCharacter(CodingErrorAction.REPLACE);
	}

	String charsetName() {
	    if (cs instanceof HistoricallyNamedCharset)
		return ((HistoricallyNamedCharset)cs).historicalName();
	    return cs.name();
	}

	byte[] encode(char[] ca, int off, int len) {
	    int en = (int)(ce.maxBytesPerChar() * len);
	    byte[] ba = new byte[en];
	    if (len == 0)
		return ba;

	    ce.reset();
	    ByteBuffer bb = ByteBuffer.wrap(ba);
	    CharBuffer cb = CharBuffer.wrap(ca, off, len);
	    try {
		CoderResult cr = ce.encode(cb, bb, true);
		if (!cr.isUnderflow())
		    cr.throwException();
		cr = ce.flush(bb);
		if (!cr.isUnderflow())
		    cr.throwException();
	    } catch (CharacterCodingException x) {
		// Substitution is always enabled,
		// so this shouldn't happen
		throw new Error(x);
	    }
	    return trim(ba, bb.position());
	}

    }

    static byte[] encode(String charsetName, char[] ca, int off, int len)
	throws UnsupportedEncodingException
    {
	StringEncoder se = (StringEncoder)deref(encoder);
	String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
	if ((se == null) || !(csn.equals(se.requestedCharsetName())
			      || csn.equals(se.charsetName()))) {
	    se = null;
	    try {
		Charset cs = lookupCharset(csn);
		if (cs != null)
		    se = new CharsetSE(cs, csn);
	    } catch (IllegalCharsetNameException x) {
		// FALL THROUGH to CharToByteConverter, for compatibility
	    }
	    if (se == null)
		se = new ConverterSE(CharToByteConverter.getConverter(csn),
				     csn);
	    set(encoder, se);
	}
	return se.encode(ca, off, len);
    }

    static byte[] encode(char[] ca, int off, int len) {
	String csn = Converters.getDefaultEncodingName();
	try {
	    return encode(csn, ca, off, len);
	} catch (UnsupportedEncodingException x) {
	    warnUnsupportedCharset(csn);
	}
	try {
	    return encode("ISO-8859-1", ca, off, len);
	} catch (UnsupportedEncodingException x) {
	    throw new Error("ISO-8859-1 charset not available", x);
	}
    }

}
