/*
 * libopenraw - ifdfilecontainer.cpp
 *
 * Copyright (C) 2006-2020 Hubert Figuière
 *
 * This library is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see
 * <http://www.gnu.org/licenses/>.
 */

#include <fcntl.h>
#include <sys/types.h>
#include <memory>

#include <vector>

#include <libopenraw/debug.h>

#include "trace.hpp"
#include "ifdfilecontainer.hpp"

using namespace Debug;

namespace OpenRaw {

namespace Internals {

IfdFileContainer::IfdFileContainer(const IO::Stream::Ptr &_file, off_t _offset)
    : RawContainer(_file, _offset)
    , m_error(0)
    , m_exif_offset_correction(0)
    , m_current_dir()
    , m_dirs()
{
}

IfdFileContainer::~IfdFileContainer()
{
    m_dirs.clear();
}

IfdFileContainer::EndianType IfdFileContainer::isMagicHeader(const char *p,
                                                             int len)
{
    if (len < 4) {
        // we need at least 4 bytes to check
        return ENDIAN_NULL;
    }
    if ((p[0] == 0x49) && (p[1] == 0x49) && (p[2] == 0x2a) && (p[3] == 0x00)) {
        return ENDIAN_LITTLE;
    } else if ((p[0] == 0x4d) && (p[1] == 0x4d) && (p[2] == 0x00) &&
               (p[3] == 0x2a)) {
        return ENDIAN_BIG;
    }
    return ENDIAN_NULL;
}

int IfdFileContainer::countDirectories(void)
{
    if (m_dirs.size() == 0) {
        // FIXME check result
        bool ret = _locateDirs();
        if (!ret) {
            return -1;
        }
    }
    return m_dirs.size();
}

std::vector<IfdDir::Ref> &IfdFileContainer::directories()
{
    if (m_dirs.size() == 0) {
        countDirectories();
    }
    return m_dirs;
}

IfdDir::Ref IfdFileContainer::setDirectory(int dir)
{
    if (dir < 0) {
        // FIXME set error
        return IfdDir::Ref();
    }
    // FIXME handle negative values
    int n = countDirectories();
    if (n <= 0) {
        // FIXME set error
        return IfdDir::Ref();
    }
    // dir is signed here because we can pass negative
    // value for specific Exif IFDs.
    if (dir > (int)m_dirs.size()) {
        // FIXME set error
        return IfdDir::Ref();
    }
    m_current_dir = m_dirs[dir];
    m_current_dir->load();
    return m_current_dir;
}

size_t IfdFileContainer::getDirectoryDataSize()
{
    // TODO move to IFDirectory
    LOGDBG1("getDirectoryDataSize()\n");
    off_t dir_offset = m_current_dir->offset();
    // FIXME check error
    LOGDBG1("offset = %lld m_numTags = %d\n", (long long int)dir_offset,
            m_current_dir->numTags());
    off_t begin = dir_offset + 2 + (m_current_dir->numTags() * 12);

    LOGDBG1("begin = %lld\n", (long long int)begin);

    m_file->seek(begin, SEEK_SET);
    begin += 2;

    int32_t nextIFD = readInt32(m_file, m_endian).value_or(0);
    LOGDBG1("nextIFD = %d\n", nextIFD);
    if (nextIFD == 0) {
        // FIXME not good
        // XXX we should check the Option<> from readInt32().
    }
    return nextIFD - begin;
}

bool IfdFileContainer::locateDirsPreHook()
{
    return true;
}

bool IfdFileContainer::_locateDirs(void)
{
    if (!locateDirsPreHook()) {
        return false;
    }
    LOGDBG1("_locateDirs()\n");
    if (m_endian == ENDIAN_NULL) {
        char buf[4];
        m_file->seek(m_offset, SEEK_SET);
        m_file->read(buf, 4);
        m_endian = isMagicHeader(buf, 4);
        if (m_endian == ENDIAN_NULL) {
            // FIXME set error code
            return false;
        }
    }
    m_file->seek(m_offset + 4, SEEK_SET);
    int32_t dir_offset = readInt32(m_file, m_endian).value_or(0);
    m_dirs.clear();
    do {
        if (dir_offset != 0) {
            LOGDBG1("push offset =0x%x\n", dir_offset);

            // we assume the offset is relative to the begining of
            // the IFD.
            IfdDir::Ref dir(
                std::make_shared<IfdDir>(m_offset + dir_offset, *this, OR_IFD_OTHER));
            m_dirs.push_back(dir);

            dir_offset = dir->nextIFD();
        }
    } while (dir_offset != 0);

    LOGDBG1("# dir found = %lu\n", (LSIZE)m_dirs.size());
    return (m_dirs.size() != 0);
}
}
}
