/*   Copyright 2004 The Apache Software Foundation
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.apache.xmlbeans.impl.store;

import java.io.PrintStream;
import java.lang.ref.SoftReference;

public final class CharUtil {
    public CharUtil(int charBufSize) {
        _charBufSize = charBufSize;
    }

    public CharIterator getCharIterator(Object src, int off, int cch) {
        _charIter.init(src, off, cch);
        return _charIter;
    }

    public CharIterator getCharIterator(Object src, int off, int cch, int start) {
        _charIter.init(src, off, cch, start);
        return _charIter;
    }

    public static CharUtil getThreadLocalCharUtil() {
        SoftReference<CharUtil> softRef = tl_charUtil.get();
        CharUtil charUtil = softRef.get();
        if (charUtil == null) {
            charUtil = new CharUtil(CHARUTIL_INITIAL_BUFSIZE);
            tl_charUtil.set(new SoftReference<>(charUtil));
        }
        return charUtil;
    }

    public static void getString(StringBuffer sb, Object src, int off, int cch) {
        assert isValid(src, off, cch);

        if (cch == 0) {
            return;
        }

        if (src instanceof char[]) {
            sb.append((char[]) src, off, cch);
        } else if (src instanceof String) {
            String s = (String) src;

            if (off == 0 && cch == s.length()) {
                sb.append(s);
            } else {
                sb.append(s, off, off + cch);
            }
        } else {
            ((CharJoin) src).getString(sb, off, cch);
        }
    }

    public static void getChars(char[] chars, int start, Object src, int off, int cch) {
        assert isValid(src, off, cch);
        assert chars != null && start >= 0 && start <= chars.length;

        if (cch == 0) {
            return;
        }

        if (src instanceof char[]) {
            char[] cs = (char[]) src;
            System.arraycopy(cs, off, chars, start, cch);
        } else if (src instanceof String) {
            ((String) src).getChars(off, off + cch, chars, start);
        } else {
            ((CharJoin) src).getChars(chars, start, off, cch);
        }
    }

    public static String getString(Object src, int off, int cch) {
        assert isValid(src, off, cch);

        if (cch == 0) {
            return "";
        }

        if (src instanceof char[]) {
            return new String((char[]) src, off, cch);
        }

        if (src instanceof String) {
            String s = (String) src;

            if (off == 0 && cch == s.length()) {
                return s;
            }

            return s.substring(off, off + cch);
        }

        StringBuffer sb = new StringBuffer();

        ((CharJoin) src).getString(sb, off, cch);

        return sb.toString();
    }

    public static boolean isWhiteSpace(char ch) {
        switch (ch) {
            case ' ':
            case '\t':
            case '\n':
            case '\r':
                return true;
            default:
                return false;
        }
    }

    public final boolean isWhiteSpace(Object src, int off, int cch) {
        assert isValid(src, off, cch);

        if (cch <= 0) {
            return true;
        }

        if (src instanceof char[]) {
            for (char[] chars = (char[]) src; cch > 0; cch--) {
                if (!isWhiteSpace(chars[off++])) {
                    return false;
                }
            }

            return true;
        }

        if (src instanceof String) {
            for (String s = (String) src; cch > 0; cch--) {
                if (!isWhiteSpace(s.charAt(off++))) {
                    return false;
                }
            }

            return true;
        }

        boolean isWhite = true;

        for (_charIter.init(src, off, cch); _charIter.hasNext(); ) {
            if (!isWhiteSpace(_charIter.next())) {
                isWhite = false;
                break;
            }
        }

        _charIter.release();

        return isWhite;
    }

    public Object stripLeft(Object src, int off, int cch) {
        assert isValid(src, off, cch);

        if (cch > 0) {
            if (src instanceof char[]) {
                char[] chars = (char[]) src;

                while (cch > 0 && isWhiteSpace(chars[off])) {
                    cch--;
                    off++;
                }
            } else if (src instanceof String) {
                String s = (String) src;

                while (cch > 0 && isWhiteSpace(s.charAt(off))) {
                    cch--;
                    off++;
                }
            } else {
                int count = 0;

                for (_charIter.init(src, off, cch); _charIter.hasNext(); count++) {
                    if (!isWhiteSpace(_charIter.next())) {
                        break;
                    }
                }

                _charIter.release();

                off += count;
            }
        }

        if (cch == 0) {
            _offSrc = 0;
            _cchSrc = 0;

            return null;
        }

        _offSrc = off;
        _cchSrc = cch;

        return src;
    }

    public Object stripRight(Object src, int off, int cch) {
        assert isValid(src, off, cch);

        if (cch > 0) {
            for (_charIter.init(src, off, cch, cch); _charIter.hasPrev(); cch--) {
                if (!isWhiteSpace(_charIter.prev())) {
                    break;
                }
            }

            _charIter.release();
        }

        if (cch == 0) {
            _offSrc = 0;
            _cchSrc = 0;

            return null;
        }

        _offSrc = off;
        _cchSrc = cch;

        return src;
    }

    public Object insertChars(
        int posInsert,
        Object src, int off, int cch,
        Object srcInsert, int offInsert, int cchInsert) {
        assert isValid(src, off, cch);
        assert isValid(srcInsert, offInsert, cchInsert);
        assert posInsert >= 0 && posInsert <= cch;

        // TODO - at some point, instead of creating joins, I should
        // normalize all the text into a single buffer to stop large
        // tree's from being built when many modifications happen...

        // TODO - actually, I should see if the size of the new char
        // sequence is small enough to simply allocate a new contigous
        // sequence, either in a common char[] managed by the master,
        // or just create a new string ... this goes for remove chars
        // as well.

        if (cchInsert == 0) {
            _cchSrc = cch;
            _offSrc = off;
            return src;
        }

        if (cch == 0) {
            _cchSrc = cchInsert;
            _offSrc = offInsert;
            return srcInsert;
        }

        _cchSrc = cch + cchInsert;

        Object newSrc;

        if (_cchSrc <= MAX_COPY && canAllocate(_cchSrc)) {
            char[] c = allocate(_cchSrc);

            getChars(c, _offSrc, src, off, posInsert);
            getChars(c, _offSrc + posInsert, srcInsert, offInsert, cchInsert);
            getChars(c, _offSrc + posInsert + cchInsert, src, off + posInsert, cch - posInsert);

            newSrc = c;
        } else {
            _offSrc = 0;

            CharJoin newJoin;

            if (posInsert == 0) {
                newJoin = new CharJoin(srcInsert, offInsert, cchInsert, src, off);
            } else if (posInsert == cch) {
                newJoin = new CharJoin(src, off, cch, srcInsert, offInsert);
            } else {
                CharJoin j = new CharJoin(src, off, posInsert, srcInsert, offInsert);
                newJoin = new CharJoin(j, 0, posInsert + cchInsert, src, off + posInsert);
            }

            if (newJoin._depth > CharJoin.MAX_DEPTH) {
                newSrc = saveChars(newJoin, _offSrc, _cchSrc);
            } else {
                newSrc = newJoin;
            }
        }

        assert isValid(newSrc, _offSrc, _cchSrc);

        return newSrc;
    }

    public Object removeChars(int posRemove, int cchRemove, Object src, int off, int cch) {
        assert isValid(src, off, cch);
        assert posRemove >= 0 && posRemove <= cch;
        assert cchRemove >= 0 && posRemove + cchRemove <= cch;

        Object newSrc;

        _cchSrc = cch - cchRemove;

        if (_cchSrc == 0) {
            newSrc = null;
            _offSrc = 0;
        } else if (posRemove == 0) {
            newSrc = src;
            _offSrc = off + cchRemove;
        } else if (posRemove + cchRemove == cch) {
            newSrc = src;
            _offSrc = off;
        } else {
            int cchAfter = cch - cchRemove;

            if (cchAfter <= MAX_COPY && canAllocate(cchAfter)) {
                char[] chars = allocate(cchAfter);

                getChars(chars, _offSrc, src, off, posRemove);


                getChars(
                    chars, _offSrc + posRemove,
                    src, off + posRemove + cchRemove, cch - posRemove - cchRemove);

                newSrc = chars;
                // _offSrc = _offSrc;
            } else {
                CharJoin j = new CharJoin(src, off, posRemove, src, off + posRemove + cchRemove);

                if (j._depth > CharJoin.MAX_DEPTH) {
                    newSrc = saveChars(j, 0, _cchSrc);
                } else {
                    newSrc = j;
                    _offSrc = 0;
                }
            }
        }

        assert isValid(newSrc, _offSrc, _cchSrc);

        return newSrc;
    }

    private boolean canAllocate(int cch) {
        return _currentBuffer == null || _currentBuffer.length - _currentOffset >= cch;
    }

    private char[] allocate(int cch) {
        assert _currentBuffer == null || _currentBuffer.length - _currentOffset > 0;

        if (_currentBuffer == null) {
            _currentBuffer = new char[Math.max(cch, _charBufSize)];
            _currentOffset = 0;
        }

        _offSrc = _currentOffset;
        _cchSrc = Math.min(_currentBuffer.length - _currentOffset, cch);

        char[] retBuf = _currentBuffer;

        assert _currentOffset + _cchSrc <= _currentBuffer.length;

        if ((_currentOffset += _cchSrc) == _currentBuffer.length) {
            _currentBuffer = null;
            _currentOffset = 0;
        }

        return retBuf;
    }

    public Object saveChars(Object srcSave, int offSave, int cchSave) {
        return saveChars(srcSave, offSave, cchSave, null, 0, 0);
    }

    public Object saveChars(
        Object srcSave, int offSave, int cchSave,
        Object srcPrev, int offPrev, int cchPrev) {
        // BUGBUG (ericvas)
        //
        // There is a severe degenerate situation which can deveol here.  The cases is where
        // there is a long strings of calls to saveChars, where the caller passes in prev text
        // to be prepended.  In this cases, the buffer breaks and a join is made, but because the
        // join is created, subsequent calls willproduce additional joins.  I need to figure
        // out a way that a whole bunch of joins are not created.  I really only want to create
        // joins in situations where large amount of text is manipulated.

        assert isValid(srcSave, offSave, cchSave);
        assert isValid(srcPrev, offPrev, cchPrev);

        // Allocate some space to save the text and copy it there.  This may not allocate all
        // the space I need.  This happens when I run out of buffer space.  Deal with this later.

        char[] srcAlloc = allocate(cchSave);
        int offAlloc = _offSrc;
        int cchAlloc = _cchSrc;

        assert cchAlloc <= cchSave;

        getChars(srcAlloc, offAlloc, srcSave, offSave, cchAlloc);

        Object srcNew;
        int offNew;

        int cchNew = cchAlloc + cchPrev;

        // The prev arguments specify a chunk of text which the caller wants prepended to the
        // text to be saved.  The optimization here is to detect the case where the prev text
        // and the newly allcoated and saved text are adjacent, so that I can avoid copying
        // or joining the two pieces.  The situation where this happens most is when a parser
        // reports a big piece of text in chunks, perhaps because there are entities in the
        // big chunk of text.

        CharJoin j;

        if (cchPrev == 0) {
            srcNew = srcAlloc;
            offNew = offAlloc;
        } else if (srcPrev == srcAlloc && offPrev + cchPrev == offAlloc) {
            //noinspection ConstantConditions
            assert srcPrev instanceof char[];

            srcNew = srcPrev;
            offNew = offPrev;
        } else if (srcPrev instanceof CharJoin && (j = (CharJoin) srcPrev)._srcRight == srcAlloc &&
                   offPrev + cchPrev - j._cchLeft + j._offRight == offAlloc) {
            assert j._srcRight instanceof char[];

            srcNew = srcPrev;
            offNew = offPrev;
        } else {
            j = new CharJoin(srcPrev, offPrev, cchPrev, srcAlloc, offAlloc);

            offNew = 0;
            srcNew = j._depth > CharJoin.MAX_DEPTH ? saveChars(j, 0, cchNew) : j;
        }

        // Now, srcNew and offNew specify the two parts of the triple which has the prev text and
        // part of the text to save (if not all of it).  Here I compute cchMore which is any
        // remaining text which was not allocated for earlier.  Effectively, this code deals with
        // the case where the text to save was greater than the remaining space in the buffer and
        // I need to allocate another buffer to save away the second part and then join the two.

        int cchMore = cchSave - cchAlloc;

        if (cchMore > 0) {
            // If we're here the buffer got consumed.  So, this time it must allocate a new
            // buffer capable of containing all of the remaining text (no matter how large) and
            // return the beginning part of it.

            srcAlloc = allocate(cchMore);
            offAlloc = _offSrc;
            cchAlloc = _cchSrc;

            assert cchAlloc == cchMore;
            assert offAlloc == 0;

            getChars(srcAlloc, offAlloc, srcSave, offSave + (cchSave - cchMore), cchMore);

            j = new CharJoin(srcNew, offNew, cchNew, srcAlloc, offAlloc);

            offNew = 0;
            cchNew += cchMore;
            srcNew = j._depth > CharJoin.MAX_DEPTH ? saveChars(j, 0, cchNew) : j;
        }

        _offSrc = offNew;
        _cchSrc = cchNew;

        assert isValid(srcNew, _offSrc, _cchSrc);

        return srcNew;
    }

    private static void dumpText(PrintStream o, String s) {
        o.print("\"");

        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);

            if (i == 36) {
                o.print("...");
                break;
            }

            switch (ch) {
                case '\n':
                    o.print("\\n");
                    break;
                case '\r':
                    o.print("\\r");
                    break;
                case '\t':
                    o.print("\\t");
                    break;
                case '\f':
                    o.print("\\f");
                    break;
                case '"':
                    o.print("\\\"");
                    break;
                default:
                    o.print(ch);
                    break;
            }
        }

        o.print("\"");
    }

    public static void dump(Object src, int off, int cch) {
        dumpChars(System.out, src, off, cch);
        System.out.println();
    }

    public static void dumpChars(PrintStream p, Object src, int off, int cch) {
        p.print("off=" + off + ", cch=" + cch + ", ");

        if (src == null) {
            p.print("<null-src>");
        } else if (src instanceof String) {
            String s = (String) src;

            p.print("String");

            if (off != 0 || cch != s.length()) {
                if (off < 0 || off > s.length() || off + cch < 0 || off + cch > s.length()) {
                    p.print(" (Error)");
                    return;
                }
            }

            //p.print( ": " );
            dumpText(p, s.substring(off, off + cch));
        } else if (src instanceof char[]) {
            char[] chars = (char[]) src;

            p.print("char[]");

            if (off != 0 || cch != chars.length) {
                if (off < 0 || off > chars.length || off + cch < 0 || off + cch > chars.length) {
                    p.print(" (Error)");
                    return;
                }
            }

            //p.print( ": " );
            dumpText(p, new String(chars, off, cch));
        } else if (src instanceof CharJoin) {
            p.print("CharJoin");

            ((CharJoin) src).dumpChars(p, off, cch);
        } else {
            p.print("Unknown text source");
        }
    }

    public static boolean isValid(Object src, int off, int cch) {
        if (cch < 0 || off < 0) {
            return false;
        }

        if (src == null) {
            return off == 0 && cch == 0;
        }

        if (src instanceof char[]) {
            char[] c = (char[]) src;
            return off <= c.length && off + cch <= c.length;
        }

        if (src instanceof String) {
            String s = (String) src;
            return off <= s.length() && off + cch <= s.length();
        }

        if (src instanceof CharJoin) {
            return ((CharJoin) src).isValid(off, cch);
        }

        return false;
    }

    //
    // Private stuff
    //

    public static final class CharJoin {
        public CharJoin(
            Object srcLeft, int offLeft, int cchLeft, Object srcRight, int offRight) {
            _srcLeft = srcLeft;
            _offLeft = offLeft;
            _cchLeft = cchLeft;
            _srcRight = srcRight;
            _offRight = offRight;

            int depth = 0;

            if (srcLeft instanceof CharJoin) {
                depth = ((CharJoin) srcLeft)._depth;
            }

            if (srcRight instanceof CharJoin) {
                int rightDepth = ((CharJoin) srcRight)._depth;

                if (rightDepth > depth) {
                    depth = rightDepth;
                }
            }

            _depth = depth + 1;

            assert _depth <= MAX_DEPTH + 2;
        }

        private int cchRight(int off, int cch) {
            return Math.max(0, cch - _cchLeft - off);
        }

        public int depth() {
            int depth = 0;

            if (_srcLeft instanceof CharJoin) {
                depth = ((CharJoin) _srcLeft).depth();
            }

            if (_srcRight instanceof CharJoin) {
                depth = Math.max(((CharJoin) _srcRight).depth(), depth);
            }

            return depth + 1;
        }

        public boolean isValid(int off, int cch) {
            // Deep trees cause this to take forever

            if (_depth > 2) {
                return true;
            }

            assert _depth == depth();

            if (off < 0 || cch < 0) {
                return false;
            }

            if (!CharUtil.isValid(_srcLeft, _offLeft, _cchLeft)) {
                return false;
            }

            return CharUtil.isValid(_srcRight, _offRight, cchRight(off, cch));
        }

        private void getString(StringBuffer sb, int off, int cch) {
            assert cch > 0;

            if (off < _cchLeft) {
                int cchL = Math.min(_cchLeft - off, cch);

                CharUtil.getString(sb, _srcLeft, _offLeft + off, cchL);

                if (cch > cchL) {
                    CharUtil.getString(sb, _srcRight, _offRight, cch - cchL);
                }
            } else {
                CharUtil.getString(sb, _srcRight, _offRight + off - _cchLeft, cch);
            }
        }

        private void getChars(char[] chars, int start, int off, int cch) {
            assert cch > 0;

            if (off < _cchLeft) {
                int cchL = Math.min(_cchLeft - off, cch);

                CharUtil.getChars(chars, start, _srcLeft, _offLeft + off, cchL);

                if (cch > cchL) {
                    CharUtil.getChars(chars, start + cchL, _srcRight, _offRight, cch - cchL);
                }
            } else {
                CharUtil.getChars(chars, start, _srcRight, _offRight + off - _cchLeft, cch);
            }
        }

        private void dumpChars(PrintStream p, int off, int cch) {
            p.print("( ");
            CharUtil.dumpChars(p, _srcLeft, _offLeft, _cchLeft);
            p.print(", ");
            CharUtil.dumpChars(p, _srcRight, _offRight, cchRight(off, cch));
            p.print(" )");
        }

        //
        //
        //

        public final Object _srcLeft;
        public final int _offLeft;
        public final int _cchLeft;

        public final Object _srcRight;
        public final int _offRight;

        public final int _depth;

        static final int MAX_DEPTH = 64;
    }

    //
    //
    //

    public final static class CharIterator {
        public void init(Object src, int off, int cch) {
            init(src, off, cch, 0);
        }

        public void init(Object src, int off, int cch, int startPos) {
            assert isValid(src, off, cch);

            release();

            _srcRoot = src;
            _offRoot = off;
            _cchRoot = cch;

            _minPos = _maxPos = -1;

            movePos(startPos);
        }

        public void release() {
            _srcRoot = null;
            _srcLeafString = null;
            _srcLeafChars = null;
        }

        public boolean hasNext() {
            return _pos < _cchRoot;
        }

        public boolean hasPrev() {
            return _pos > 0;
        }

        public char next() {
            assert hasNext();

            char ch = currentChar();

            movePos(_pos + 1);

            return ch;
        }

        public char prev() {
            assert hasPrev();

            movePos(_pos - 1);

            return currentChar();
        }

        public void movePos(int newPos) {
            assert newPos >= 0 && newPos <= _cchRoot;

            if (newPos < _minPos || newPos > _maxPos) {
                // if newPos out of cached leaf, recache new leaf
                Object src = _srcRoot;
                int off = _offRoot + newPos;
                int cch = _cchRoot;

                for (_offLeaf = _offRoot; src instanceof CharJoin; ) {
                    CharJoin j = (CharJoin) src;

                    if (off < j._cchLeft) {
                        src = j._srcLeft;
                        _offLeaf = j._offLeft;
                        off = off + j._offLeft;
                        cch = j._cchLeft;
                    } else {
                        src = j._srcRight;
                        _offLeaf = j._offRight;
                        off = off - (j._cchLeft - j._offRight);
                        cch = cch - j._cchLeft;
                    }
                }

//                _offLeaf = off - Math.min( off - _offLeaf, newPos );
                _minPos = newPos - (off - _offLeaf);
//                _maxPos = newPos + Math.min( _cchRoot - newPos, sizeof( src ) - off );
                _maxPos = _minPos + cch;

                if (newPos < _cchRoot) {
                    _maxPos--;
                }

                // Cache the leaf src to avoid instanceof for every char

                _srcLeafChars = null;
                _srcLeafString = null;

                if (src instanceof char[]) {
                    _srcLeafChars = (char[]) src;
                } else {
                    _srcLeafString = (String) src;
                }

                assert newPos >= _minPos && newPos <= _maxPos;
            }

            _pos = newPos;
        }

        private char currentChar() {
            int i = _offLeaf + _pos - _minPos;

            return _srcLeafChars == null ? _srcLeafString.charAt(i) : _srcLeafChars[i];
        }

        private Object _srcRoot; // Original triple
        private int _offRoot;
        private int _cchRoot;

        private int _pos;     // Current position

        private int _minPos;  // Min/max poses for current cached leaf
        private int _maxPos;

        private int _offLeaf;

        private String _srcLeafString;  // Cached leaf - either a char[] or a string
        private char[] _srcLeafChars;
    }

    private static final int CHARUTIL_INITIAL_BUFSIZE = 1024 * 32;
    private static final ThreadLocal<SoftReference<CharUtil>> tl_charUtil =
        ThreadLocal.withInitial(() -> new SoftReference<>(new CharUtil(CHARUTIL_INITIAL_BUFSIZE)));

    public static void clearThreadLocals() {
        tl_charUtil.remove();
    }

    private final CharIterator _charIter = new CharIterator();

    // TODO - 64 is kinda arbitrary.  Perhaps it should be configurable.
    private static final int MAX_COPY = 64;

    // Current char buffer we're allcoating new chars to

    private final int _charBufSize;
    private int _currentOffset;
    private char[] _currentBuffer;

    // These members are used to communicate offset and character count
    // information back to a caller of various methods on CharUtil.
    // Usually, the methods returns the src Object, and these two hold
    // the offset and the char count.

    public int _offSrc;
    public int _cchSrc;
}
