/*
 * (C) Copyright IBM Corp. 1997-1998  All rights reserved.
 *
 * US Government Users Restricted Rights Use, duplication or
 * disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
 *
 * The program is provided "as is" without any warranty express or
 * implied, including the warranty of non-infringement and the implied
 * warranties of merchantibility and fitness for a particular purpose.
 * IBM will not be liable for any damages suffered by you as a result
 * of using the Program. In no event will IBM be liable for any
 * special, indirect or consequential damages or lost profits even if
 * IBM has been advised of the possibility of their occurrence. IBM
 * will not be liable for any third party claims against you.
 */

package com.ibm.xml.parser;

import java.io.BufferedReader;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PushbackInputStream;
import java.io.Reader;
import java.io.UTFDataFormatException;
import java.io.UnsupportedEncodingException;

/**
 *
 * @version 1.1
 * @author TAMURA Kent &lt;kent@trl.ibm.co.jp&gt;
 */
final class XMLReader extends Reader implements Reading {
    private Parser m_pc;
    private static final int
    	E_UCS4B = 0,            // UCS-4 big endian
    	E_UCS4L = 1,            // UCS-4 little endian
	E_UCS2B = 2,            // UCS-2 big endian with byte order mark
	E_UCS2L = 3,            // UCS-2 little endian with byte order mark
	E_UCS2B_NOBOM = 4,      // UCS-2 big endian without byte order mark
	E_UCS2L_NOBOM = 5,      // UCS-2 little endian without byte order mark
    	E_ASCIICOMPAT = 6,      // ASCII compatible code (include UTF-8, ISO-2022-JP, ...)
    	E_UTF8_NOXMLDECL = 7;   // UTF-8 without "<?xml ..."
    private static final String classname = "com.ibm.xml.parser.XMLReader";
    private String eofmessage;
    private int m_encoding;
    private InputStream m_is = null;
    private Reader m_rr;
    private Source m_source;
    private String m_head = null;
    private String tail = null;


    private int m_linenumber = 1;
    private int m_charlocation = 0;
    private int m_linenumber_pre = 1;
    private int m_charlocation_pre = 0;

    private int m_lastchar = -1;
    private boolean m_resetline = false;

    int position = Token.ST_NORMAL;
    int state = 0;
    int paren = 0;
    boolean ltgt = false;

    public XMLReader(Parser pc, String filename, Source src)
        throws InvalidEncodingException, IOException {
        this(pc, filename, src, null);
    }

    public XMLReader(Parser pc, String filename, Source src, String head)
        throws InvalidEncodingException, IOException {
        this.m_head = head;
        this.m_source = src;
        this.m_fname = filename;
	this.m_pc = pc;
        this.eofmessage = this.classname+"#read: "+this.m_pc.getString("E_EOF");

        if (null != src.getReader()) {
            m_rr = makeReader(src.getReader());
            m_nextchar = read();
            return;
        } else if (null != src.getEncoding()) {
            m_rr = makeReader(new InputStreamReader(src.getInputStream(), src.getEncoding()));
            m_nextchar = read();
            return;
        }
        InputStream is = src.getInputStream();
        byte[] f4c = new byte[4]; // first 4 Byte of stream
        int f4clen = 0;
        int ch;
        if (0 <= (ch = is.read()))  f4c[f4clen++] = (byte)ch;
        if (0 <= (ch = is.read()))  f4c[f4clen++] = (byte)ch;
        if (0 <= (ch = is.read()))  f4c[f4clen++] = (byte)ch;
        if (0 <= (ch = is.read()))  f4c[f4clen++] = (byte)ch;
        
	if (4 == f4clen && 0 == f4c[0] && 0 == f4c[1] && 0 == f4c[2] && '<' == f4c[3]) {
	    m_encoding = E_UCS4B;
            m_head = "<";
            m_rr = makeReader(new UCSReader(is, m_encoding));
	} else if (4 == f4clen && '<' == f4c[0] && 0 == f4c[1] && 0 == f4c[2] && 0 == f4c[3]) {
	    m_encoding = E_UCS4L;
            m_head = "<";
            m_rr = makeReader(new UCSReader(is, m_encoding));
	} else if (2 <= f4clen && (byte)0xfe == f4c[0] && (byte)0xff == f4c[1]) {
	    m_encoding = E_UCS2B;
            if (3 == f4clen)  new InvalidEncodingException(m_pc.getString("E_INVENC0"));
            if (4 == f4clen) {
                char[] ac = new char[1];
                ac[0] = (char)(((f4c[2]<<8)&0xff00)+f4c[3]);
                m_head = new String(ac);
            }
            m_rr = makeReader(new UCSReader(is, m_encoding));
	} else if (2 <= f4clen && (byte)0xff == f4c[0] && (byte)0xfe == f4c[1]) {
	    m_encoding = E_UCS2L;
            if (3 == f4clen)  new InvalidEncodingException(m_pc.getString("E_INVENC0"));
            if (4 == f4clen) {
                char[] ac = new char[1];
                ac[0] = (char)(((f4c[3]<<8)&0xff00)+f4c[2]);
                m_head = new String(ac);
            }
            m_rr = makeReader(new UCSReader(is, m_encoding));
	} else if (4 == f4clen && 0 == f4c[0] && '<' == f4c[1] && 0 == f4c[2] && '?' == f4c[3]) {
	    if (m_pc.isErrorNoByteMark()) {
		throw new InvalidEncodingException(XMLReader.classname+"#constructor: "
                                                   +m_pc.getString("E_INVENC0"));
	    }
	    m_encoding = E_UCS2B_NOBOM;
            m_head = "<?";
            m_rr = makeReader(new UCSReader(is, m_encoding));
	} else if (4 == f4clen && '<' == f4c[0] && 0 == f4c[1] && '?' == f4c[2] && 0 == f4c[3]) {
	    if (m_pc.isErrorNoByteMark()) {
		throw new InvalidEncodingException(XMLReader.classname+"#constructor: "
                                                   +m_pc.getString("E_INVENC1"));
	    }
	    m_encoding = E_UCS2L_NOBOM;
            m_head = "<?";
            m_rr = makeReader(new UCSReader(is, m_encoding));
	} else if (4 == f4clen && '<' == f4c[0] && '?' == f4c[1] && 'x' == f4c[2] && 'm' == f4c[3]) {
	    m_encoding = E_ASCIICOMPAT;
            m_head = "<?xm";
	} else {
	    m_encoding = E_UTF8_NOXMLDECL;
            try {
                if (0 == f4clen) {
                    m_head = null;
                } else if (0 < f4clen && f4clen < 4) {
                    m_head = new String(f4c, 0, f4clen, "UTF8");
                } else {                        // f4clen == 4
                    PushbackInputStream pis = new PushbackInputStream(is, 4);
                    pis.unread(f4c);
                    is = pis;
                    m_head = null;
                }
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace(); // Internal Error
            }
            if (null != m_head && 0 == m_head.length())  m_head = null;
            m_rr = makeReader(new UTF8Reader(is));
	}
        m_is = is;

        m_nextchar = read();
    }

    /**
     *
     */
    private Reader makeReader(Reader rr) {
        int size = m_pc.getReaderBufferSize();
        return 0 < size ? new BufferedReader(rr, size) : rr;
    }

    /**
     * Called by parser when the parser find encoding="" in <?xml ...?>.
     */ 
    public void setEncoding(String enc) throws UnsupportedEncodingException {
        setEncoding(enc, false);
    }
    /**
     *
     */
    public void setEncoding(String enc, boolean allowj) throws UnsupportedEncodingException {
        if (null != m_rr)  return;
        enc = enc.toUpperCase();
        if ("ISO-10646-UCS-2".equals(enc))  return;
        if ("ISO-10646-UCS-4".equals(enc))  return;
        if ("UTF-16".equals(enc))  return;

        String javaencname = MIME2Java.convert(enc);
        if (null == javaencname) {
                                                // Not supported
            if (allowj) {
                javaencname = enc;
            } else {
                throw new UnsupportedEncodingException(XMLReader.classname+"#setEncoding: "
                                                       +m_pc.format1("E_ENC0", enc));
            }
        }
        try {
            if ("UTF-8".equalsIgnoreCase(javaencname) || "UTF8".equalsIgnoreCase(javaencname))
                m_rr = makeReader(new UTF8Reader(m_is));
            else
                m_rr = makeReader(new InputStreamReader(m_is, javaencname));
        } catch (UnsupportedEncodingException e) {
            throw e;
        } catch (Exception e) {
            e.printStackTrace();            // Internal Error
        }
    }

    /**
     *
     */
    public int read(char cbuf[], int off, int len) throws IOException {
        int ret = 0;
        synchronized (lock) {
            while (ret < len) {
                int ch = read();
                if (0 > ch)  break;
                cbuf[off+ret] = (char)ch;
                ret ++;
            }
        }
        return 0 >= ret ? -1 : ret;
    }

    class UCSReader extends Reader {
        InputStream m_is;
        int m_encoding;
        int m_prevchar = -1;
        int m_nextchar = -1;

        UCSReader(InputStream is, int type) {
            m_is = is;
            m_encoding = type;
        }

        public int read(char cbuf[], int off, int len) throws IOException {
            int ret = 0;
            synchronized (this.lock) {
                for (int i = 0;  i < len;  i ++) {
                    if (0 <= m_nextchar) {
                        cbuf[off+i] = (char)m_nextchar;
                        m_nextchar = -1;
                        ret ++;
                        continue;
                    }
                    int ch = -1;
                    int b1 = m_is.read();
                    if (0 > b1 && 0 == i) {
                        ret = -1;
                        break;
                    }
                    switch (m_encoding) {
                      case E_UCS4B:
                          {
                              int b2, b3, b4;
                              if (0 > b1)  break;
                              b2 = m_is.read();
                              if (0 > b2)
                                  throw new EOFException(eofmessage);
                              b3 = m_is.read();
                              if (0 > b3)
                                  throw new EOFException(eofmessage);
                              b4 = m_is.read();
                              if (0 > b4)
                                  throw new EOFException(eofmessage);
                              ch = (b1<<24)+(b2<<16)+(b3<<8)+b4;

                              if (0x10ffff < ch)
                                  throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                                   +m_pc.format1("E_ENC1", Integer.toHexString(ch)));
                              if (0x010000 <= ch) {
                                  int h = ((ch-0x00010000)>>10)+0xd800;
                                  int l = ((ch-0x00010000)&0x3ff)+0xdc00;
                                  ch = h;
                                  m_nextchar = l;
                              }
                          }
                          break;

                      case E_UCS4L:
                          {
                              int b2, b3, b4;
                              if (0 > b1)  break;
                              b2 = m_is.read();
                              if (0 > b2)
                                  throw new EOFException(eofmessage);
                              b3 = m_is.read();
                              if (0 > b3)
                                  throw new EOFException(eofmessage);
                              b4 = m_is.read();
                              if (0 > b4)
                                  throw new EOFException(eofmessage);
                              ch = (b4<<24)+(b3<<16)+(b2<<8)+b1;

                              if (0x10ffff < ch)
                                  throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                                   +m_pc.format1("E_ENC1", Integer.toHexString(ch)));
                              if (0x010000 <= ch) {
                                  int h = ((ch-0x00010000)>>10)+0xd800;
                                  int l = ((ch-0x00010000)&0x3ff)+0xdc00;
                                  ch = h;
                                  m_nextchar = l;
                              }
                          }
                          break;

                      case E_UCS2B:
                      case E_UCS2B_NOBOM:
                          {
                              int b2;
                              if (0 > b1)  break;
                              b2 = m_is.read();
                              if (0 > b2)
                                  throw new EOFException(eofmessage);
                              ch = (b1<<8)+b2;
                          }
                          if (0 <= m_prevchar) {
                              if (!(0xdc00 <= ch && ch < 0xe000))
                                  throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                                   +m_pc.format2("E_ENC2", Integer.toHexString(m_prevchar), Integer.toHexString(ch)));
                              m_prevchar = -1;
                          }
                          if (0xd800 <= ch && ch < 0xdc00) {
                              m_prevchar = ch;
                          } else if (0xdc00 <= ch && ch < 0xe000) {
                              throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                               +m_pc.format1("E_ENC3", Integer.toHexString(ch)));
                          }
                          break;

                      case E_UCS2L:
                      case E_UCS2L_NOBOM:
                          {
                              int b2;
                              if (0 > b1)  break;
                              b2 = m_is.read();
                              if (0 > b2)
                                  throw new EOFException(eofmessage);
                              ch = (b2<<8)+b1;
                          }
                          if (0 <= m_prevchar) {
                              if (!(0xdc00 <= ch && ch < 0xe000))
                                  throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                                   +m_pc.format2("E_ENC2", Integer.toHexString(m_prevchar), Integer.toHexString(ch)));
                              m_prevchar = -1;
                          }
                          if (0xd800 <= ch && ch < 0xdc00) {
                              m_prevchar = ch;
                          } else if (0xdc00 <= ch && ch < 0xe000) {
                              throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                               +m_pc.format1("E_ENC3", Integer.toHexString(ch)));
                          }
                          break;
                    }

                    if (ch < 0)  break;
                    cbuf[off+i] = (char)ch;
                    ret ++;
                } // for
            } // synchronized
            return ret;
        }
        public void close() throws IOException {
            m_is.close();
            m_is = null;
        }
    }

    class UTF8Reader extends Reader {
        InputStream m_is;
        int m_prevchar = -1;
        int m_nextchar = -1;

        UTF8Reader(InputStream is) {
            m_is = is;
        }

        public int read(char cbuf[], int off, int len) throws IOException {
            int ret = 0;
            synchronized (this.lock) {
                for (int i = 0;  i < len;  i ++) {
                    if (0 <= m_nextchar) {
                        cbuf[off+i] = (char)m_nextchar;
                        m_prevchar = -1;
                        m_nextchar = -1;
                        ret ++;
                        continue;
                    }

                    int ch = -1;

                    int b0 = m_is.read();
                    if (0 <= b0) {
                        if ((0x80 & b0) == 0)   // 0xxx xxxx
                            ch = b0;
                        else {                  // 1xxx xxxx
                            int b1 = m_is.read();
                            if (0 > b1)
                                throw new EOFException(eofmessage);
                            if ((0xc0 & b1) != 0x80)
                                throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                                 +m_pc.format2("E_ENC5", Integer.toHexString(b0), Integer.toHexString(b1)));
                            if ((0xe0 & b0) == 0xc0) { // 110x xxxx
                                ch = ((0x1f & b0)<<6)+(0x3f & b1);
                            } else if ((0xf0 & b0) == 0xe0) { // 1110 xxxx
                                int b2 = m_is.read();
                                if (0 > b2)
                                    throw new EOFException(eofmessage);
                                if ((0xc0 & b2) != 0x80)
                                    throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                                     +m_pc.format3("E_ENC6", Integer.toHexString(b0), Integer.toHexString(b1), Integer.toHexString(b2)));
                                ch = ((0x0f & b0)<<12) + ((0x3f & b1)<<6) + (0x3f & b2);
                            } else if ((0xf8 & b0) == 0xf0) { // 1111 0xxx
                                int b2 = m_is.read();
                                if (0 > b2)
                                    throw new EOFException(eofmessage);
                                if ((0xc0 & b2) != 0x80)
                                    throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                                     +m_pc.format3("E_ENC6", Integer.toHexString(b0), Integer.toHexString(b1), Integer.toHexString(b2)));
                                int b3 = m_is.read();
                                if (0 > b3)
                                    throw new EOFException(eofmessage);
                                if ((0xc0 & b3) != 0x80)
                                    throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                                     +m_pc.format4("E_ENC7", Integer.toHexString(b0), Integer.toHexString(b1), Integer.toHexString(b2), Integer.toHexString(b3)));
                                ch = ((0x0f & b0)<<18) + ((0x3f & b1)<<12)
                                    + ((0x3f & b2)<<6) + (0x3f & b3);
                                if (0x010000 <= ch) {
                                    int h = ((ch-0x00010000)>>10)+0xd800;
                                    int l = ((ch-0x00010000)&0x3ff)+0xdc00;
                                    ch = h;
                                    m_nextchar = l;
                                }
                            } else if ((0xfc & b0) == 0xf8) { // 1111 10xx 5Byte format
                                throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                                 +m_pc.format1("E_ENC4", Integer.toHexString(b0)));
                            } else if ((0xfe & b0) == 0xfc) { // 1111 110x 6Byte format
                                throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                                 +m_pc.format1("E_ENC4", Integer.toHexString(b0)));
                            } else {
                                throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                                 +m_pc.format1("E_ENC4", Integer.toHexString(b0)));
                            }
                        }
                    } else if (0 == i) {
                        ret = -1;
                        break;
                    } else
                        break;
                    if (0 <= m_prevchar) {
                        if (!(0xdc00 <= ch && ch < 0xe000))
                            throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                             +m_pc.format2("E_ENC2", Integer.toHexString(m_prevchar), Integer.toHexString(ch)));
                        m_prevchar = -1;
                    }
                    if (0xd800 <= ch && ch < 0xdc00) {
                        m_prevchar = ch;
                    } else if (0xdc00 <= ch && ch < 0xe000) {
                        throw new UTFDataFormatException(XMLReader.classname+"#read: "
                                                         +m_pc.format1("E_ENC3", Integer.toHexString(ch)));
                    }
                    cbuf[off+i] = (char)ch;
                    ret ++;
                } // for
            } // synchronized
            return ret;
        }

        public void close() throws IOException {
            m_is.close();
            m_is = null;
        }
    }

    public void addTail(String tail) {
        if (this.tail == null) {
            this.tail = tail;
        } else {
            this.tail = this.tail+tail;
        }
    }

    public int read() throws IOException {
        int ret = -1;
        //synchronized (lock) {
            if (null != m_head) {
                ret = m_head.charAt(0);
                m_head = 1 == m_head.length() ? null : m_head.substring(1);
            } else {
                if (null != m_rr) {
                    ret = m_rr.read();
                } else if (null != m_is) {
                    ret = m_is.read();
                } else {
                    ret = -1;
                }
                if (ret < 0) {
                    if (this.tail != null) {
                        ret = this.tail.charAt(0);
                        this.tail = this.tail.length() == 1 ? null : this.tail.substring(1);
                    } else
                        return ret;
                }
            }

            m_linenumber_pre = m_linenumber;
            m_charlocation_pre = m_charlocation;
            if (m_resetline) {
                m_linenumber ++;
                m_charlocation = 0;
                m_resetline = false;
            }
            m_charlocation ++;
            if (0x0d == ret || (0x0a == ret && 0x0d != m_lastchar)) {
                m_resetline = true;
            }
            m_lastchar = ret;
        //}
        return ret;
    }

    public void close() throws IOException {
        if (null != m_rr) {
            m_rr.close();
            m_rr = null;
            m_is = null;
        } else if (null != m_is) {
            m_is.close();
            m_is = null;
        }
        if (null != m_pc)
            m_pc.closeInputStream(m_source);
        m_pc = null;
        m_source = null;
    }

    public int getLineNumber() {
        return m_linenumber_pre;
    }
    public int getCharLocation() {
        return m_charlocation_pre;
    }

    int m_nextchar = -1;
    Reading m_nextr = null;
    private String m_fname;
    public int getChar() throws IOException {
        int ch = m_nextchar;
        m_nextchar = read();
        if (this.position != Token.ST_NORMAL && this.position != Token.ST_VALUE) {
            if (this.position == Token.ST_INTERNALDTD || this.position == Token.ST_EXTERNALDTD) {
                if (this.state != 0) {
                    if (this.state == ch)
                        this.state = 0;
                } else if (ch == '<') {
                    this.paren ++;
                } else if (ch == '>') {
                    this.paren --;
                } else if (ch == '"' || ch == '\'') {
                    this.state = ch;
                }
            } else {                            // ST_TAG or ST_CONTENTMODEL
                /*
                this.length ++;
                if (!XMLChar.isSpace(ch)) {
                    if (this.firstchar < 0)  this.firstchar = ch;
                    this.lastchar = ch;
                }
                */
                if (this.state != 0) {
                    if (this.state == ch)
                        this.state = 0;
                } else if (ch == '(') {
                    this.paren ++;
                } else if (ch == ')') {
                    this.paren --;
                } else if (ch == '>' || ch == '<') {
                    this.ltgt = true;
                } else if (ch == '"' || ch == '\'') {
                    this.state = ch;
                }
            }
        }
        return ch;
    }
    public String getFileName() {
        return m_fname;
    }
    public void setNext(Reading r) {
        m_nextr = r;
    }
    public Reading getNext() {
        return m_nextr;
    }
    public int readNext() {
        return m_nextchar;
    }

    public void setChecking(int status) {
        this.position = status;
    }
    public String getErrorKey() {
        String ret = null;
        if (this.ltgt) {
            ret = "E_PEREF9";
        } else if (this.paren != 0 && this.position == Token.ST_CONTENTMODEL) {
            ret = "E_PEREF7";
        } else if (this.paren != 0) {
            ret = "E_PEREF9";
            /*
        } else if (this.position == Token.ST_CONTENTMODEL
                   && (this.firstchar == '|' || this.firstchar == ','
                       || this.lastchar == '|' || this.lastchar == ',')) {
            ret = "E_PEREF8";
        } else if (this.position == Token.ST_CONTENTMODEL
                   && this.length == 2) {
            ret = "E_PEREF6";
            */
        }
        return ret;
    }
}
