/* ====================================================================
   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You 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.poi.hssf.record;

import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.util.HexDump;
import org.apache.poi.util.HexRead;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianInput;
import org.apache.poi.util.LittleEndianOutput;
import org.apache.poi.util.POILogFactory;
import org.apache.poi.util.POILogger;
import org.apache.poi.util.RecordFormatException;
import org.apache.poi.util.StringUtil;

/**
 * The <code>HyperlinkRecord</code> (0x01B8) wraps an HLINK-record
 *  from the Excel-97 format.
 * Supports only external links for now (eg http://)
 */
public final class HyperlinkRecord extends StandardRecord implements Cloneable {
    public final static short sid = 0x01B8;
    private static POILogger logger = POILogFactory.getLogger(HyperlinkRecord.class);
    //arbitrarily selected; may need to increase
    private static final int MAX_RECORD_LENGTH = 100_000;


    static final class GUID {
		/*
		 * this class is currently only used here, but could be moved to a
		 * common package if needed
		 */
		private static final int TEXT_FORMAT_LENGTH = 36;

		public static final int ENCODED_SIZE = 16;

		/** 4 bytes - little endian */
		private final int _d1;
		/** 2 bytes - little endian */
		private final int _d2;
		/** 2 bytes - little endian */
		private final int _d3;
		/**
		 * 8 bytes - serialized as big endian,  stored with inverted endianness here
		 */
		private final long _d4;

		public GUID(LittleEndianInput in) {
			this(in.readInt(), in.readUShort(), in.readUShort(), in.readLong());
		}

		public GUID(int d1, int d2, int d3, long d4) {
			_d1 = d1;
			_d2 = d2;
			_d3 = d3;
			_d4 = d4;
		}

		public void serialize(LittleEndianOutput out) {
			out.writeInt(_d1);
			out.writeShort(_d2);
			out.writeShort(_d3);
			out.writeLong(_d4);
		}

		@Override
		public boolean equals(Object obj) {
            if (!(obj instanceof GUID)) {
                return false;
            }
			GUID other = (GUID) obj;
			return _d1 == other._d1 && _d2 == other._d2
			    && _d3 == other._d3 && _d4 == other._d4;
		}

       @Override
       public int hashCode() {
           assert false : "hashCode not designed";
           return 42; // any arbitrary constant will do
       }

       public int getD1() {
			return _d1;
		}

		public int getD2() {
			return _d2;
		}

		public int getD3() {
			return _d3;
		}

		public long getD4() {
		    byte[] result = new byte[Long.SIZE/Byte.SIZE];
		    long l = _d4;
		    for (int i = result.length-1; i >= 0; i--) {
		        result[i] = (byte)(l & 0xFF);
		        l >>= 8;
		    }
		    
			return LittleEndian.getLong(result, 0);
		}

		public String formatAsString() {

			StringBuilder sb = new StringBuilder(36);

			int PREFIX_LEN = "0x".length();
			sb.append(HexDump.intToHex(_d1).substring(PREFIX_LEN));
			sb.append("-");
			sb.append(HexDump.shortToHex(_d2).substring(PREFIX_LEN));
			sb.append("-");
			sb.append(HexDump.shortToHex(_d3).substring(PREFIX_LEN));
			sb.append("-");
			String d4Chars = HexDump.longToHex(getD4());
			sb.append(d4Chars.substring(PREFIX_LEN, PREFIX_LEN+4));
			sb.append("-");
			sb.append(d4Chars.substring(PREFIX_LEN+4));
			return sb.toString();
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder(64);
			sb.append(getClass().getName()).append(" [");
			sb.append(formatAsString());
			sb.append("]");
			return sb.toString();
		}

		/**
		 * Read a GUID in standard text form e.g.<br>
		 * 13579BDF-0246-8ACE-0123-456789ABCDEF 
		 * <br> -&gt; <br>
		 *  0x13579BDF, 0x0246, 0x8ACE 0x0123456789ABCDEF
		 */
		public static GUID parse(String rep) {
			char[] cc = rep.toCharArray();
			if (cc.length != TEXT_FORMAT_LENGTH) {
				throw new RecordFormatException("supplied text is the wrong length for a GUID");
			}
			int d0 = (parseShort(cc, 0) << 16) + (parseShort(cc, 4) << 0);
			int d1 = parseShort(cc, 9);
			int d2 = parseShort(cc, 14);
			for (int i = 23; i > 19; i--) {
				cc[i] = cc[i - 1];
			}
			long d3 = parseLELong(cc, 20);

			return new GUID(d0, d1, d2, d3);
		}

		private static long parseLELong(char[] cc, int startIndex) {
			long acc = 0;
			for (int i = startIndex + 14; i >= startIndex; i -= 2) {
				acc <<= 4;
				acc += parseHexChar(cc[i + 0]);
				acc <<= 4;
				acc += parseHexChar(cc[i + 1]);
			}
			return acc;
		}

		private static int parseShort(char[] cc, int startIndex) {
			int acc = 0;
			for (int i = 0; i < 4; i++) {
				acc <<= 4;
				acc += parseHexChar(cc[startIndex + i]);
			}
			return acc;
		}

		private static int parseHexChar(char c) {
			if (c >= '0' && c <= '9') {
				return c - '0';
			}
			if (c >= 'A' && c <= 'F') {
				return c - 'A' + 10;
			}
			if (c >= 'a' && c <= 'f') {
				return c - 'a' + 10;
			}
			throw new RecordFormatException("Bad hex char '" + c + "'");
		}
	}

    /**
     * Link flags
     */
     static final int  HLINK_URL    = 0x01;  // File link or URL.
     static final int  HLINK_ABS    = 0x02;  // Absolute path.
     static final int  HLINK_LABEL  = 0x14;  // Has label/description.
    /** Place in worksheet. If set, the {@link #_textMark} field will be present */
     static final int  HLINK_PLACE  = 0x08;
    private static final int  HLINK_TARGET_FRAME  = 0x80;  // has 'target frame'
    private static final int  HLINK_UNC_PATH  = 0x100;  // has UNC path

     final static GUID STD_MONIKER = GUID.parse("79EAC9D0-BAF9-11CE-8C82-00AA004BA90B");
     final static GUID URL_MONIKER = GUID.parse("79EAC9E0-BAF9-11CE-8C82-00AA004BA90B");
     final static GUID FILE_MONIKER = GUID.parse("00000303-0000-0000-C000-000000000046");
    /** expected Tail of a URL link */
    private final static byte[] URL_TAIL  = HexRead.readFromString("79 58 81 F4  3B 1D 7F 48   AF 2C 82 5D  C4 85 27 63   00 00 00 00  A5 AB 00 00"); 
    /** expected Tail of a file link */
    private final static byte[] FILE_TAIL = HexRead.readFromString("FF FF AD DE  00 00 00 00   00 00 00 00  00 00 00 00   00 00 00 00  00 00 00 00");

    private static final int TAIL_SIZE = FILE_TAIL.length;

    /** cell range of this hyperlink */
    private CellRangeAddress _range;

    /** 16-byte GUID */
    private GUID _guid;
    /** Some sort of options for file links. */
    private int _fileOpts;
    /** Link options. Can include any of HLINK_* flags. */
    private int _linkOpts;
    /** Test label */
    private String _label;

    private String _targetFrame;
    /** Moniker. Makes sense only for URL and file links */
    private GUID _moniker;
    /** in 8:3 DOS format No Unicode string header,
     * always 8-bit characters, zero-terminated */
    private String _shortFilename;
    /** Link */
    private String _address;
    /**
     * Text describing a place in document.  In Excel UI, this is appended to the
     * address, (after a '#' delimiter).<br>
     * This field is optional.  If present, the {@link #HLINK_PLACE} must be set.
     */
    private String _textMark;
    
    private byte[] _uninterpretedTail;

    /**
     * Create a new hyperlink
     */
    public HyperlinkRecord()
    {

    }

    /**
     * @return the 0-based column of the first cell that contains this hyperlink
     */
    public int getFirstColumn() {
        return _range.getFirstColumn();
    }

    /**
     * Set the first column (zero-based) of the range that contains this hyperlink
     * 
     * @param firstCol the first column (zero-based)
     */
    public void setFirstColumn(int firstCol) {
        _range.setFirstColumn(firstCol);
    }

    /**
     * @return the 0-based column of the last cell that contains this hyperlink
     */
    public int getLastColumn() {
        return _range.getLastColumn();
    }

    /**
     * Set the last column (zero-based) of the range that contains this hyperlink
     * 
     * @param lastCol the last column (zero-based)
     */
    public void setLastColumn(int lastCol) {
        _range.setLastColumn(lastCol);
    }

    /**
     * @return the 0-based row of the first cell that contains this hyperlink
     */
    public int getFirstRow() {
        return _range.getFirstRow();
    }

    /**
     * Set the first row (zero-based) of the range that contains this hyperlink
     * 
     * @param firstRow the first row (zero-based)
     */
    public void setFirstRow(int firstRow) {
        _range.setFirstRow(firstRow);
    }

    /**
     * @return the 0-based row of the last cell that contains this hyperlink
     */
    public int getLastRow() {
        return _range.getLastRow();
    }

    /**
     * Set the last row (zero-based) of the range that contains this hyperlink
     * 
     * @param lastRow the last row (zero-based)
     */
    public void setLastRow(int lastRow) {
        _range.setLastRow(lastRow);
    }

    /**
     * @return 16-byte guid identifier Seems to always equal {@link #STD_MONIKER}
     */
    GUID getGuid() {
        return _guid;
    }

    /**
     * @return 16-byte moniker
     */
    GUID getMoniker()
    {
        return _moniker;
    }

    private static String cleanString(String s) {
        if (s == null) {
            return null;
        }
        int idx = s.indexOf('\u0000');
        if (idx < 0) {
            return s;
        }
        return s.substring(0, idx);
    }
    private static String appendNullTerm(String s) {
        if (s == null) {
            return null;
        }
        return s + '\u0000';
    }

    /**
     * Return text label for this hyperlink
     *
     * @return  text to display
     */
    public String getLabel() {
        return cleanString(_label);
    }

    /**
     * Sets text label for this hyperlink
     *
     * @param label text label for this hyperlink
     */
    public void setLabel(String label) {
        _label = appendNullTerm(label);
    }
    public String getTargetFrame() {
        return cleanString(_targetFrame);
    }

    /**
     * Hyperlink address. Depending on the hyperlink type it can be URL, e-mail, path to a file, etc.
     *
     * @return  the address of this hyperlink
     */
    public String getAddress() {
        if ((_linkOpts & HLINK_URL) != 0 && FILE_MONIKER.equals(_moniker)) {
            return cleanString(_address != null ? _address : _shortFilename);
        } else if((_linkOpts & HLINK_PLACE) != 0) {
            return cleanString(_textMark);
        } else {
            return cleanString(_address);
        }
    }

    /**
     * Hyperlink address. Depending on the hyperlink type it can be URL, e-mail, path to a file, etc.
     *
     * @param address  the address of this hyperlink
     */
    public void setAddress(String address) {
        if ((_linkOpts & HLINK_URL) != 0 && FILE_MONIKER.equals(_moniker)) {
            _shortFilename = appendNullTerm(address);
        } else if((_linkOpts & HLINK_PLACE) != 0) {
            _textMark = appendNullTerm(address);
        } else {
            _address = appendNullTerm(address);
        }
    }

    public String getShortFilename() {
        return cleanString(_shortFilename);
    }

    public void setShortFilename(String shortFilename) {
        _shortFilename = appendNullTerm(shortFilename);
    }

    public String getTextMark() {
        return cleanString(_textMark);
    }
    public void setTextMark(String textMark) {
        _textMark = appendNullTerm(textMark);
    }


    /**
     * Link options. Must be a combination of HLINK_* constants.
     * For testing only
     * 
     * @return Link options
     */
    int getLinkOptions(){
        return _linkOpts;
    }

    /**
     * @return Label options
     */
    public int getLabelOptions(){
        return 2; // always 2
    }

    /**
     * @return Options for a file link
     */
    public int getFileOptions(){
        return _fileOpts;
    }


    public HyperlinkRecord(RecordInputStream in) {
        _range = new CellRangeAddress(in);

        _guid = new GUID(in);

        /**
         * streamVersion (4 bytes): An unsigned integer that specifies the version number
         * of the serialization implementation used to save this structure. This value MUST equal 2.
         */
        int streamVersion = in.readInt();
        if (streamVersion != 0x00000002) {
            throw new RecordFormatException("Stream Version must be 0x2 but found " + streamVersion);
        }
        _linkOpts = in.readInt();

        if ((_linkOpts & HLINK_LABEL) != 0){
            int label_len = in.readInt();
            _label = in.readUnicodeLEString(label_len);
        }

        if ((_linkOpts & HLINK_TARGET_FRAME) != 0){
            int len = in.readInt();
            _targetFrame = in.readUnicodeLEString(len);
        }

        if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) != 0) {
            _moniker = null;
            int nChars = in.readInt();
            _address = in.readUnicodeLEString(nChars);
        }

        if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) == 0) {
            _moniker = new GUID(in);

            if(URL_MONIKER.equals(_moniker)){
                int length = in.readInt();
                /**
                 * The value of <code>length<code> be either the byte size of the url field
                 * (including the terminating NULL character) or the byte size of the url field plus 24.
                 * If the value of this field is set to the byte size of the url field,
                 * then the tail bytes fields are not present.
                 */
                int remaining = in.remaining();
                if (length == remaining) {
                    int nChars = length/2;
                    _address = in.readUnicodeLEString(nChars);
                } else {
                    int nChars = (length - TAIL_SIZE)/2;
                    _address = in.readUnicodeLEString(nChars);
                    /**
                     * TODO: make sense of the remaining bytes
                     * According to the spec they consist of:
                     * 1. 16-byte  GUID: This field MUST equal
                     *    {0xF4815879, 0x1D3B, 0x487F, 0xAF, 0x2C, 0x82, 0x5D, 0xC4, 0x85, 0x27, 0x63}
                     * 2. Serial version, this field MUST equal 0 if present.
                     * 3. URI Flags
                     */
                    _uninterpretedTail = readTail(URL_TAIL, in);
                }
            } else if (FILE_MONIKER.equals(_moniker)) {
                _fileOpts = in.readShort();

                int len = in.readInt();
                _shortFilename = StringUtil.readCompressedUnicode(in, len);
                _uninterpretedTail = readTail(FILE_TAIL, in);
                int size = in.readInt();
                if (size > 0) {
                    int charDataSize = in.readInt();

                    //From the spec: An optional unsigned integer that MUST be 3 if present
                    // but some files has 4
                    /*int usKeyValue = */ in.readUShort();

                    _address = StringUtil.readUnicodeLE(in, charDataSize/2);
                } else {
                    _address = null;
                }
            } else if (STD_MONIKER.equals(_moniker)) {
                _fileOpts = in.readShort();

                int len = in.readInt();

                byte[] path_bytes = IOUtils.safelyAllocate(len, MAX_RECORD_LENGTH);
                in.readFully(path_bytes);

                _address = new String(path_bytes, StringUtil.UTF8);
            }
        }

        if((_linkOpts & HLINK_PLACE) != 0) {

            int len = in.readInt();
            _textMark = in.readUnicodeLEString(len);
        }

        if (in.remaining() > 0) {
           logger.log(POILogger.WARN, 
                 "Hyperlink data remains: " + in.remaining() +
                 " : " +HexDump.toHex(in.readRemainder())
           );
        }
    }

    @Override
    public void serialize(LittleEndianOutput out) {
        _range.serialize(out);

        _guid.serialize(out);
        out.writeInt(0x00000002); // TODO const
        out.writeInt(_linkOpts);

        if ((_linkOpts & HLINK_LABEL) != 0){
            out.writeInt(_label.length());
            StringUtil.putUnicodeLE(_label, out);
        }
        if ((_linkOpts & HLINK_TARGET_FRAME) != 0){
            out.writeInt(_targetFrame.length());
            StringUtil.putUnicodeLE(_targetFrame, out);
        }

        if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) != 0) {
            out.writeInt(_address.length());
            StringUtil.putUnicodeLE(_address, out);
        }

        if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) == 0) {
            _moniker.serialize(out);
            if(URL_MONIKER.equals(_moniker)){
                if (_uninterpretedTail == null) {
                    out.writeInt(_address.length()*2);
                    StringUtil.putUnicodeLE(_address, out);
                } else {
                    out.writeInt(_address.length()*2 + TAIL_SIZE);
                    StringUtil.putUnicodeLE(_address, out);
                    writeTail(_uninterpretedTail, out);
                }
            } else if (FILE_MONIKER.equals(_moniker)){
                out.writeShort(_fileOpts);
                out.writeInt(_shortFilename.length());
                StringUtil.putCompressedUnicode(_shortFilename, out);
                writeTail(_uninterpretedTail, out);
                if (_address == null) {
                    out.writeInt(0);
                } else {
                    int addrLen = _address.length() * 2;
                    out.writeInt(addrLen + 6);
                    out.writeInt(addrLen);
                    out.writeShort(0x0003); // TODO const
                    StringUtil.putUnicodeLE(_address, out);
                }
            }
        }
        if((_linkOpts & HLINK_PLACE) != 0){
               out.writeInt(_textMark.length());
            StringUtil.putUnicodeLE(_textMark, out);
        }
    }

    @Override
    protected int getDataSize() {
        int size = 0;
        size += 2 + 2 + 2 + 2;  //rwFirst, rwLast, colFirst, colLast
        size += GUID.ENCODED_SIZE;
        size += 4;  //label_opts
        size += 4;  //link_opts
        if ((_linkOpts & HLINK_LABEL) != 0){
            size += 4;  //link length
            size += _label.length()*2;
        }
        if ((_linkOpts & HLINK_TARGET_FRAME) != 0){
            size += 4;  // int nChars
            size += _targetFrame.length()*2;
        }
        if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) != 0) {
            size += 4;  // int nChars
            size += _address.length()*2;
        }
        if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) == 0) {
            size += GUID.ENCODED_SIZE;
            if(URL_MONIKER.equals(_moniker)){
                size += 4;  //address length
                size += _address.length()*2;
                if (_uninterpretedTail != null) {
                	size += TAIL_SIZE;
                }
            } else if (FILE_MONIKER.equals(_moniker)){
                size += 2;  //file_opts
                size += 4;  //address length
                size += _shortFilename.length();
                size += TAIL_SIZE;
                size += 4;
                if (_address != null) {
                    size += 6;
                    size += _address.length() * 2;
                }

            }
        }
        if((_linkOpts & HLINK_PLACE) != 0){
            size += 4;  //address length
            size += _textMark.length()*2;
        }
        return size;
    }


    private static byte[] readTail(byte[] expectedTail, LittleEndianInput in) {
    	byte[] result = new byte[TAIL_SIZE];
    	in.readFully(result);
//    	if (false) { // Quite a few examples in the unit tests which don't have the exact expected tail
//            for (int i = 0; i < expectedTail.length; i++) {
//                if (expectedTail[i] != result[i]) {
//                	logger.log( POILogger.ERROR, "Mismatch in tail byte [" + i + "]"
//                    		+ "expected " + (expectedTail[i] & 0xFF) + " but got " + (result[i] & 0xFF));
//                }
//            }
//    	}
        return result;
    }
    private static void writeTail(byte[] tail, LittleEndianOutput out) {
        out.write(tail);
    }

    @Override
    public short getSid() {
        return HyperlinkRecord.sid;
    }


    @Override
    public String toString() {
        StringBuffer buffer = new StringBuffer();

        buffer.append("[HYPERLINK RECORD]\n");
        buffer.append("    .range   = ").append(_range.formatAsString()).append("\n");
        buffer.append("    .guid    = ").append(_guid.formatAsString()).append("\n");
        buffer.append("    .linkOpts= ").append(HexDump.intToHex(_linkOpts)).append("\n");
        buffer.append("    .label   = ").append(getLabel()).append("\n");
        if ((_linkOpts & HLINK_TARGET_FRAME) != 0) {
            buffer.append("    .targetFrame= ").append(getTargetFrame()).append("\n");
        }
        if((_linkOpts & HLINK_URL) != 0 && _moniker != null) {
            buffer.append("    .moniker   = ").append(_moniker.formatAsString()).append("\n");
        }
        if ((_linkOpts & HLINK_PLACE) != 0) {
            buffer.append("    .textMark= ").append(getTextMark()).append("\n");
        }
        buffer.append("    .address   = ").append(getAddress()).append("\n");
        buffer.append("[/HYPERLINK RECORD]\n");
        return buffer.toString();
    }

    /**
     * Based on the link options, is this a url?
     * 
     * @return true, if this is a url link
     */
    public boolean isUrlLink() {
       return (_linkOpts & HLINK_URL) > 0 
           && (_linkOpts & HLINK_ABS) > 0;
    }
    /**
     * Based on the link options, is this a file?
     * 
     * @return true, if this is a file link
     */
    public boolean isFileLink() {
       return (_linkOpts & HLINK_URL) > 0 
           && (_linkOpts & HLINK_ABS) == 0;
    }
    /**
     * Based on the link options, is this a document?
     * 
     * @return true, if this is a docment link
     */
    public boolean isDocumentLink() {
       return (_linkOpts & HLINK_PLACE) > 0; 
    }
    
    /**
     * Initialize a new url link
     */
    public void newUrlLink() {
        _range = new CellRangeAddress(0, 0, 0, 0);
        _guid = STD_MONIKER;
        _linkOpts = HLINK_URL | HLINK_ABS | HLINK_LABEL;
        setLabel("");
        _moniker = URL_MONIKER;
        setAddress("");
        _uninterpretedTail = URL_TAIL;
    }

    /**
     * Initialize a new file link
     */
    public void newFileLink() {
        _range = new CellRangeAddress(0, 0, 0, 0);
        _guid = STD_MONIKER;
        _linkOpts = HLINK_URL | HLINK_LABEL;
        _fileOpts = 0;
        setLabel("");
        _moniker = FILE_MONIKER;
        setAddress(null);
        setShortFilename("");
        _uninterpretedTail = FILE_TAIL;
    }

    /**
     * Initialize a new document link
     */
    public void newDocumentLink() {
        _range = new CellRangeAddress(0, 0, 0, 0);
        _guid = STD_MONIKER;
        _linkOpts = HLINK_LABEL | HLINK_PLACE;
        setLabel("");
        _moniker = FILE_MONIKER;
        setAddress("");
        setTextMark("");
    }

    @Override
    public HyperlinkRecord clone() {
        HyperlinkRecord rec = new HyperlinkRecord();
        rec._range = _range.copy();
        rec._guid = _guid;
        rec._linkOpts = _linkOpts;
        rec._fileOpts = _fileOpts;
        rec._label = _label;
        rec._address = _address;
        rec._moniker = _moniker;
        rec._shortFilename = _shortFilename;
        rec._targetFrame = _targetFrame;
        rec._textMark = _textMark;
        rec._uninterpretedTail = _uninterpretedTail;
        return rec;
    }
}
