/*
 * barrier -- mouse and keyboard sharing utility
 * Copyright (C) 2016 Symless Ltd.
 *
 * This package is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * found in the file LICENSE that should have accompanied this file.
 *
 * This package is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "platform/OSXUchrKeyResource.h"

#include <Carbon/Carbon.h>

//
// OSXUchrKeyResource
//

OSXUchrKeyResource::OSXUchrKeyResource(const void* resource,
                UInt32 keyboardType) :
    m_m(NULL),
    m_cti(NULL),
    m_sdi(NULL),
    m_sri(NULL),
    m_st(NULL)
{
    m_resource = static_cast<const UCKeyboardLayout*>(resource);
    if (m_resource == NULL) {
        return;
    }

    // find the keyboard info for the current keyboard type
    const UCKeyboardTypeHeader* th = NULL;
    const UCKeyboardLayout* r = m_resource;
    for (ItemCount i = 0; i < r->keyboardTypeCount; ++i) {
        if (keyboardType >= r->keyboardTypeList[i].keyboardTypeFirst &&
            keyboardType <= r->keyboardTypeList[i].keyboardTypeLast) {
            th = r->keyboardTypeList + i;
            break;
        }
        if (r->keyboardTypeList[i].keyboardTypeFirst == 0) {
            // found the default.  use it unless we find a match.
            th = r->keyboardTypeList + i;
        }
    }
    if (th == NULL) {
        // cannot find a suitable keyboard type
        return;
    }

    // get tables for keyboard type
    const UInt8* const base = reinterpret_cast<const UInt8*>(m_resource);
    m_m   = reinterpret_cast<const UCKeyModifiersToTableNum*>(base +
                                th->keyModifiersToTableNumOffset);
    m_cti = reinterpret_cast<const UCKeyToCharTableIndex*>(base +
                                th->keyToCharTableIndexOffset);
    m_sdi = reinterpret_cast<const UCKeySequenceDataIndex*>(base +
                                th->keySequenceDataIndexOffset);
    if (th->keyStateRecordsIndexOffset != 0) {
        m_sri = reinterpret_cast<const UCKeyStateRecordsIndex*>(base +
                                th->keyStateRecordsIndexOffset);
    }
    if (th->keyStateTerminatorsOffset != 0) {
        m_st = reinterpret_cast<const UCKeyStateTerminators*>(base +
                                th->keyStateTerminatorsOffset);
    }

    // find the space key, but only if it can combine with dead keys.
    // a dead key followed by a space yields the non-dead version of
    // the dead key.
    m_spaceOutput = 0xffffu;
    UInt32 table  = getTableForModifier(0);
    for (UInt32 button = 0, n = getNumButtons(); button < n; ++button) {
        KeyID id = getKey(table, button);
        if (id == 0x20) {
            UCKeyOutput c =
                reinterpret_cast<const UCKeyOutput*>(base +
                                m_cti->keyToCharTableOffsets[table])[button];
            if ((c & kUCKeyOutputTestForIndexMask) ==
                                kUCKeyOutputStateIndexMask) {
                m_spaceOutput = (c & kUCKeyOutputGetIndexMask);
                break;
            }
        }
    }
}

bool
OSXUchrKeyResource::isValid() const
{
    return (m_m != NULL);
}

UInt32
OSXUchrKeyResource::getNumModifierCombinations() const
{
    // only 32 (not 256) because the righthanded modifier bits are ignored
    return 32;
}

UInt32
OSXUchrKeyResource::getNumTables() const
{
    return m_cti->keyToCharTableCount;
}

UInt32
OSXUchrKeyResource::getNumButtons() const
{
    return m_cti->keyToCharTableSize;
}

UInt32
OSXUchrKeyResource::getTableForModifier(UInt32 mask) const
{
    if (mask >= m_m->modifiersCount) {
        return m_m->defaultTableNum;
    }
    else {
        return m_m->tableNum[mask];
    }
}

KeyID
OSXUchrKeyResource::getKey(UInt32 table, UInt32 button) const
{
    assert(table < getNumTables());
    assert(button < getNumButtons());

    const UInt8* const base   = reinterpret_cast<const UInt8*>(m_resource);
    const UCKeyOutput* cPtr = reinterpret_cast<const UCKeyOutput*>(base +
                                m_cti->keyToCharTableOffsets[table]);

  const UCKeyOutput c = cPtr[button];

    KeySequence keys;
    switch (c & kUCKeyOutputTestForIndexMask) {
    case kUCKeyOutputStateIndexMask:
        if (!getDeadKey(keys, c & kUCKeyOutputGetIndexMask)) {
            return kKeyNone;
        }
        break;

    case kUCKeyOutputSequenceIndexMask:
    default:
        if (!addSequence(keys, c)) {
            return kKeyNone;
        }
        break;
    }

    // XXX -- no support for multiple characters
    if (keys.size() != 1) {
        return kKeyNone;
    }

    return keys.front();
}

bool
OSXUchrKeyResource::getDeadKey(
    KeySequence& keys, UInt16 index) const
{
    if (m_sri == NULL || index >= m_sri->keyStateRecordCount) {
        // XXX -- should we be using some other fallback?
        return false;
    }

    UInt16 state = 0;
    if (!getKeyRecord(keys, index, state)) {
        return false;
    }
    if (state == 0) {
        // not a dead key
        return true;
    }

    // no dead keys if we couldn't find the space key
    if (m_spaceOutput == 0xffffu) {
        return false;
    }

    // the dead key should not have put anything in the key list
    if (!keys.empty()) {
        return false;
    }

    // get the character generated by pressing the space key after the
    // dead key.  if we're still in a compose state afterwards then we're
    // confused so we bail.
    if (!getKeyRecord(keys, m_spaceOutput, state) || state != 0) {
        return false;
    }

    // convert keys to their dead counterparts
    for (KeySequence::iterator i = keys.begin(); i != keys.end(); ++i) {
        *i = barrier::KeyMap::getDeadKey(*i);
    }

    return true;
}

bool
OSXUchrKeyResource::getKeyRecord(
    KeySequence& keys, UInt16 index, UInt16& state) const
{
    const UInt8* const base = reinterpret_cast<const UInt8*>(m_resource);
    const UCKeyStateRecord* sr =
        reinterpret_cast<const UCKeyStateRecord*>(base +
                                m_sri->keyStateRecordOffsets[index]);
    const UCKeyStateEntryTerminal* kset =
        reinterpret_cast<const UCKeyStateEntryTerminal*>(sr->stateEntryData);

    UInt16 nextState = 0;
    bool found       = false;
    if (state == 0) {
        found     = true;
        nextState = sr->stateZeroNextState;
        if (!addSequence(keys, sr->stateZeroCharData)) {
            return false;
        }
    }
    else {
        // we have a next entry
        switch (sr->stateEntryFormat) {
        case kUCKeyStateEntryTerminalFormat:
            for (UInt16 j = 0; j < sr->stateEntryCount; ++j) {
                if (kset[j].curState == state) {
                    if (!addSequence(keys, kset[j].charData)) {
                        return false;
                    }
                    nextState = 0;
                    found     = true;
                    break;
                }
            }
            break;

        case kUCKeyStateEntryRangeFormat:
            // XXX -- not supported yet
            break;

        default:
            // XXX -- unknown format
            return false;
        }
    }
    if (!found) {
        // use a terminator
        if (m_st != NULL && state < m_st->keyStateTerminatorCount) {
            if (!addSequence(keys, m_st->keyStateTerminators[state - 1])) {
                return false;
            }
        }
        nextState = sr->stateZeroNextState;
        if (!addSequence(keys, sr->stateZeroCharData)) {
            return false;
        }
    }

    // next
    state = nextState;

    return true;
}

bool
OSXUchrKeyResource::addSequence(
    KeySequence& keys, UCKeyCharSeq c) const
{
    if ((c & kUCKeyOutputTestForIndexMask) == kUCKeyOutputSequenceIndexMask) {
        UInt16 index = (c & kUCKeyOutputGetIndexMask);
        if (index < m_sdi->charSequenceCount &&
            m_sdi->charSequenceOffsets[index] !=
                m_sdi->charSequenceOffsets[index + 1]) {
            // XXX -- sequences not supported yet
            return false;
        }
    }

    if (c != 0xfffe && c != 0xffff) {
        KeyID id = unicharToKeyID(c);
        if (id != kKeyNone) {
            keys.push_back(id);
        }
    }

    return true;
}
