'use strict';

// adapted from http://code.google.com/p/plist/source/browse/trunk/src/main/java/com/dd/plist/BinaryPropertyListWriter.java

var streamBuffers = require("stream-buffers");

var debug = false;

function Real(value) {
  this.value = value;
}

module.exports = function(dicts) {
  var buffer = new streamBuffers.WritableStreamBuffer();
  buffer.write(new Buffer("bplist00"));

  if (debug) {
    console.log('create', require('util').inspect(dicts, false, 10));
  }

  if (dicts instanceof Array && dicts.length === 1) {
    dicts = dicts[0];
  }

  var entries = toEntries(dicts);
  if (debug) {
    console.log('entries', entries);
  }
  var idSizeInBytes = computeIdSizeInBytes(entries.length);
  var offsets = [];
  var offsetSizeInBytes;
  var offsetTableOffset;

  updateEntryIds();

  entries.forEach(function(entry, entryIdx) {
    offsets[entryIdx] = buffer.size();
    if (!entry) {
      buffer.write(0x00);
    } else {
      write(entry);
    }
  });

  writeOffsetTable();
  writeTrailer();
  return buffer.getContents();

  function updateEntryIds() {
    var strings = {};
    var entryId = 0;
    entries.forEach(function(entry) {
      if (entry.id) {
        return;
      }
      if (entry.type === 'string') {
        if (!entry.bplistOverride && strings.hasOwnProperty(entry.value)) {
          entry.type = 'stringref';
          entry.id = strings[entry.value];
        } else {
          strings[entry.value] = entry.id = entryId++;
        }
      } else {
        entry.id = entryId++;
      }
    });

    entries = entries.filter(function(entry) {
      return (entry.type !== 'stringref');
    });
  }

  function writeTrailer() {
    if (debug) {
      console.log('0x' + buffer.size().toString(16), 'writeTrailer');
    }
    // 6 null bytes
    buffer.write(new Buffer([0, 0, 0, 0, 0, 0]));

    // size of an offset
    if (debug) {
      console.log('0x' + buffer.size().toString(16), 'writeTrailer(offsetSizeInBytes):', offsetSizeInBytes);
    }
    writeByte(offsetSizeInBytes);

    // size of a ref
    if (debug) {
      console.log('0x' + buffer.size().toString(16), 'writeTrailer(offsetSizeInBytes):', idSizeInBytes);
    }
    writeByte(idSizeInBytes);

    // number of objects
    if (debug) {
      console.log('0x' + buffer.size().toString(16), 'writeTrailer(number of objects):', entries.length);
    }
    writeLong(entries.length);

    // top object
    if (debug) {
      console.log('0x' + buffer.size().toString(16), 'writeTrailer(top object)');
    }
    writeLong(0);

    // offset table offset
    if (debug) {
      console.log('0x' + buffer.size().toString(16), 'writeTrailer(offset table offset):', offsetTableOffset);
    }
    writeLong(offsetTableOffset);
  }

  function writeOffsetTable() {
    if (debug) {
      console.log('0x' + buffer.size().toString(16), 'writeOffsetTable');
    }
    offsetTableOffset = buffer.size();
    offsetSizeInBytes = computeOffsetSizeInBytes(offsetTableOffset);
    offsets.forEach(function(offset) {
      writeBytes(offset, offsetSizeInBytes);
    });
  }

  function write(entry) {
    switch (entry.type) {
    case 'dict':
      writeDict(entry);
      break;
    case 'number':
    case 'double':
      writeNumber(entry);
      break;
    case 'UID':
      writeUID(entry);
      break;
    case 'array':
      writeArray(entry);
      break;
    case 'boolean':
      writeBoolean(entry);
      break;
    case 'string':
    case 'string-utf16':
      writeString(entry);
      break;
    case 'date':
      writeDate(entry);
      break;
    case 'data':
      writeData(entry);
      break;
    default:
      throw new Error("unhandled entry type: " + entry.type);
    }
  }

  function writeDate(entry) {
    writeByte(0x33);
    var date = (Date.parse(entry.value)/1000) - 978307200
    writeDouble(date)
  }

  function writeDict(entry) {
    if (debug) {
      var keysStr = entry.entryKeys.map(function(k) {return k.id;});
      var valsStr = entry.entryValues.map(function(k) {return k.id;});
      console.log('0x' + buffer.size().toString(16), 'writeDict', '(id: ' + entry.id + ')', '(keys: ' + keysStr + ')', '(values: ' + valsStr + ')');
    }
    writeIntHeader(0xD, entry.entryKeys.length);
    entry.entryKeys.forEach(function(entry) {
      writeID(entry.id);
    });
    entry.entryValues.forEach(function(entry) {
      writeID(entry.id);
    });
  }

  function writeNumber(entry) {
    if (debug) {
      console.log('0x' + buffer.size().toString(16), 'writeNumber', entry.value, ' (type: ' + entry.type + ')', '(id: ' + entry.id + ')');
    }

    if (entry.type !== 'double' && parseFloat(entry.value.toFixed()) == entry.value) {
      if (entry.value < 0) {
        writeByte(0x13);
        writeBytes(entry.value, 8, true);
      } else if (entry.value <= 0xff) {
        writeByte(0x10);
        writeBytes(entry.value, 1);
      } else if (entry.value <= 0xffff) {
        writeByte(0x11);
        writeBytes(entry.value, 2);
      } else if (entry.value <= 0xffffffff) {
        writeByte(0x12);
        writeBytes(entry.value, 4);
      } else {
        writeByte(0x14);
        writeBytes(entry.value, 8);
      }
    } else {
      writeByte(0x23);
      writeDouble(entry.value);
    }
  }

  function writeUID(entry) {
    if (debug) {
      console.log('0x' + buffer.size().toString(16), 'writeUID', entry.value, ' (type: ' + entry.type + ')', '(id: ' + entry.id + ')');
    }

    writeIntHeader(0x8, 0x0);
    writeID(entry.value);
  }

  function writeArray(entry) {
    if (debug) {
      console.log('0x' + buffer.size().toString(16), 'writeArray (length: ' + entry.entries.length + ')', '(id: ' + entry.id + ')');
    }
    writeIntHeader(0xA, entry.entries.length);
    entry.entries.forEach(function(e) {
      writeID(e.id);
    });
  }

  function writeBoolean(entry) {
    if (debug) {
      console.log('0x' + buffer.size().toString(16), 'writeBoolean', entry.value, '(id: ' + entry.id + ')');
    }
    writeByte(entry.value ? 0x09 : 0x08);
  }

  function writeString(entry) {
    if (debug) {
      console.log('0x' + buffer.size().toString(16), 'writeString', entry.value, '(id: ' + entry.id + ')');
    }
    if (entry.type === 'string-utf16' || mustBeUtf16(entry.value)) {
      var utf16 = new Buffer(entry.value, 'ucs2');
      writeIntHeader(0x6, utf16.length / 2);
      // needs to be big endian so swap the bytes
      for (var i = 0; i < utf16.length; i += 2) {
        var t = utf16[i + 0];
        utf16[i + 0] = utf16[i + 1];
        utf16[i + 1] = t;
      }
      buffer.write(utf16);
    } else {
      var utf8 = new Buffer(entry.value, 'ascii');
      writeIntHeader(0x5, utf8.length);
      buffer.write(utf8);
    }
  }

  function writeData(entry) {
    if (debug) {
      console.log('0x' + buffer.size().toString(16), 'writeData', entry.value, '(id: ' + entry.id + ')');
    }
    writeIntHeader(0x4, entry.value.length);
    buffer.write(entry.value);
  }

  function writeLong(l) {
    writeBytes(l, 8);
  }

  function writeByte(b) {
    buffer.write(new Buffer([b]));
  }

  function writeDouble(v) {
    var buf = new Buffer(8);
    buf.writeDoubleBE(v, 0);
    buffer.write(buf);
  }

  function writeIntHeader(kind, value) {
    if (value < 15) {
      writeByte((kind << 4) + value);
    } else if (value < 256) {
      writeByte((kind << 4) + 15);
      writeByte(0x10);
      writeBytes(value, 1);
    } else if (value < 65536) {
      writeByte((kind << 4) + 15);
      writeByte(0x11);
      writeBytes(value, 2);
    } else {
      writeByte((kind << 4) + 15);
      writeByte(0x12);
      writeBytes(value, 4);
    }
  }

  function writeID(id) {
    writeBytes(id, idSizeInBytes);
  }

  function writeBytes(value, bytes, is_signedint) {
    // write low-order bytes big-endian style
    var buf = new Buffer(bytes);
    var z = 0;

    // javascript doesn't handle large numbers
    if(!is_signedint) {
      while (bytes > 4) {
        buf[z++] = 0;
        bytes--;
      }
    }

    for (var i = bytes - 1; i >= 0; i--) {
      buf[z++] = value >> (8 * i);
    }
    buffer.write(buf);
  }

  function mustBeUtf16(string) {
    return Buffer.byteLength(string, 'utf8') != string.length;
  }
};

function toEntries(dicts) {
  if (dicts.bplistOverride) {
    return [dicts];
  }

  if (dicts instanceof Array) {
    return toEntriesArray(dicts);
  } else if (dicts instanceof Buffer) {
    return [
      {
        type: 'data',
        value: dicts
      }
    ];
  } else if (dicts instanceof Real) {
    return [
      {
        type: 'double',
        value: dicts.value
      }
    ];
  } else if (typeof(dicts) === 'object') {
    if (dicts instanceof Date) {
      return [
        {
          type: 'date',
          value: dicts
        }
      ]
    } else if (Object.keys(dicts).length == 1 && typeof(dicts.UID) === 'number') {
      return [
        {
          type: 'UID',
          value: dicts.UID
        }
      ]
    } else {
      return toEntriesObject(dicts);
    }
  } else if (typeof(dicts) === 'string') {
    return [
      {
        type: 'string',
        value: dicts
      }
    ];
  } else if (typeof(dicts) === 'number') {
    return [
      {
        type: 'number',
        value: dicts
      }
    ];
  } else if (typeof(dicts) === 'boolean') {
    return [
      {
        type: 'boolean',
        value: dicts
      }
    ];
  } else if (typeof(dicts) === 'bigint') {
    return [
      {
        type: 'number',
        value: Number(BigInt.asIntN(32, dicts))
      }
    ];
  } else {
    throw new Error('unhandled entry: ' + dicts);
  }
}

function toEntriesArray(arr) {
  if (debug) {
    console.log('toEntriesArray');
  }
  var results = [
    {
      type: 'array',
      entries: []
    }
  ];
  arr.forEach(function(v) {
    var entry = toEntries(v);
    results[0].entries.push(entry[0]);
    results = results.concat(entry);
  });
  return results;
}

function toEntriesObject(dict) {
  if (debug) {
    console.log('toEntriesObject');
  }
  var results = [
    {
      type: 'dict',
      entryKeys: [],
      entryValues: []
    }
  ];
  Object.keys(dict).forEach(function(key) {
    var entryKey = toEntries(key);
    results[0].entryKeys.push(entryKey[0]);
    results = results.concat(entryKey[0]);
  });
  Object.keys(dict).forEach(function(key) {
    var entryValue = toEntries(dict[key]);
    results[0].entryValues.push(entryValue[0]);
    results = results.concat(entryValue);
  });
  return results;
}

function computeOffsetSizeInBytes(maxOffset) {
  if (maxOffset < 256) {
    return 1;
  }
  if (maxOffset < 65536) {
    return 2;
  }
  if (maxOffset < 4294967296) {
    return 4;
  }
  return 8;
}

function computeIdSizeInBytes(numberOfIds) {
  if (numberOfIds < 256) {
    return 1;
  }
  if (numberOfIds < 65536) {
    return 2;
  }
  return 4;
}

module.exports.Real = Real;
