# -*- coding: utf-8 -*-

# Copyright (C) 2012  Matias Bordese
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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/>.

import ctypes
import os
import platform

from ctypes.util import find_library

from unrar import constants


__all__ = ["RAROpenArchiveDataEx", "RARHeaderDataEx", "RAROpenArchiveEx",
           "RARCloseArchive", "RARReadHeaderEx", "RARProcessFile",
           "RARSetPassword", "RARGetDllVersion", "dostime_to_timetuple"]


lib_path = os.environ.get('UNRAR_LIB_PATH', None)

# find and load unrar library
unrarlib = None
if platform.system() == 'Windows':
    from ctypes.wintypes import HANDLE as WIN_HANDLE
    HANDLE = WIN_HANDLE
    lib_path = lib_path or find_library("unrar.dll")
    if lib_path:
        unrarlib = ctypes.WinDLL(lib_path)
else:
    # assume unix
    HANDLE = ctypes.c_void_p
    lib_path = lib_path or find_library("unrar")
    if lib_path:
        unrarlib = ctypes.cdll.LoadLibrary(lib_path)

if unrarlib is None:
    raise LookupError("Couldn't find path to unrar library.")


def dostime_to_timetuple(dostime):
    """Convert a RAR archive member DOS time to a Python time tuple."""
    dostime = dostime >> 16
    dostime = dostime & 0xffff
    day = dostime & 0x1f
    month = (dostime >> 5) & 0xf
    year = 1980 + (dostime >> 9)
    second = 2 * (dostime & 0x1f)
    minute = (dostime >> 5) & 0x3f
    hour = dostime >> 11
    return (year, month, day, hour, minute, second)


class UnrarException(Exception):
    """Lib errors exception."""


class _Structure(ctypes.Structure):
    """Customized ctypes Structure base class."""

    def __repr__(self):
        """Print the structure fields."""
        res = []
        for field in self._fields_:
            field_value = repr(getattr(self, field[0]))
            res.append('%s=%s' % (field[0], field_value))
        return self.__class__.__name__ + '(' + ','.join(res) + ')'


class RAROpenArchiveDataEx(_Structure):
    """Rar compressed file structure."""
    _fields_ = [
                ('ArcName', ctypes.c_char_p),
                ('ArcNameW', ctypes.c_wchar_p),
                ('OpenMode', ctypes.c_uint),
                ('OpenResult', ctypes.c_uint),
                ('_CmtBuf', ctypes.c_void_p),
                ('CmtBufSize', ctypes.c_uint),
                ('CmtSize', ctypes.c_uint),
                ('CmtState', ctypes.c_uint),
                ('Flags', ctypes.c_uint),
                ('Reserved', ctypes.c_uint * 32),
               ]

    def __init__(self, filename, mode=constants.RAR_OM_LIST):
        # comments buffer, max 64kb
        self.CmtBuf = ctypes.create_string_buffer('', 64 * 1024)
        super(RAROpenArchiveDataEx, self).__init__(
            ArcName=None,
            ArcNameW=filename, OpenMode=mode,
            _CmtBuf=ctypes.addressof(self.CmtBuf),
            CmtBufSize=ctypes.sizeof(self.CmtBuf))

    def __str__(self):
        return self.ArcNameW


class RARHeaderDataEx(_Structure):
    """Rar file header structure."""
    _fields_ = [
                ('ArcName', ctypes.c_char * 1024),
                ('ArcNameW', ctypes.c_wchar * 1024),
                ('FileName', ctypes.c_char * 1024),
                ('FileNameW', ctypes.c_wchar * 1024),
                ('Flags', ctypes.c_uint),
                ('PackSize', ctypes.c_uint),
                ('PackSizeHigh', ctypes.c_uint),
                ('UnpSize', ctypes.c_uint),
                ('UnpSizeHigh', ctypes.c_uint),
                ('HostOS', ctypes.c_uint),
                ('FileCRC', ctypes.c_uint),
                ('FileTime', ctypes.c_uint),
                ('UnpVer', ctypes.c_uint),
                ('Method', ctypes.c_uint),
                ('FileAttr', ctypes.c_uint),
                ('_CmtBuf', ctypes.c_void_p),
                ('CmtBufSize', ctypes.c_uint),
                ('CmtSize', ctypes.c_uint),
                ('CmtState', ctypes.c_uint),
                ('Reserved', ctypes.c_uint * 1024),
               ]

    def __init__(self):
        # comments buffer, max 64kb
        self.CmtBuf = ctypes.create_string_buffer('', 64 * 1024)
        super(RARHeaderDataEx, self).__init__(
            _CmtBuf=ctypes.addressof(self.CmtBuf),
            CmtBufSize=ctypes.sizeof(self.CmtBuf))

    def __str__(self):
        return self.FileNameW


def _c_func(func, restype, argtypes, errcheck=None):
    """Wrap c function setting prototype."""
    func.restype = restype
    func.argtypes = argtypes
    if errcheck is not None:
        func.errcheck = errcheck
    return func


def _check_open_result(res, func, args):
    if res is None:
        raise UnrarException("Archive open error")
    # res is the archive handle
    return res


def _check_readheader_result(res, func, args):
    if res == constants.ERAR_BAD_DATA:
        raise UnrarException("Bad header data.")
    # res == SUCCESS | res == ERAR_END_ARCHIVE
    return res


def _check_process_result(res, func, args):
    if res == constants.ERAR_ECLOSE:
        raise UnrarException("File close error")
    elif res == constants.ERAR_BAD_DATA:
        raise UnrarException("File CRC error")
    elif res == constants.ERAR_BAD_ARCHIVE:
        raise UnrarException("Not valid RAR archive")
    elif res == constants.ERAR_UNKNOWN_FORMAT:
        raise UnrarException("Unknown archive format")
    elif res == constants.ERAR_EOPEN:
        raise UnrarException("Volume open error")
    elif res == constants.ERAR_ECREATE:
        raise UnrarException("File create error")
    elif res == constants.ERAR_EREAD:
        raise UnrarException("Read error")
    elif res == constants.ERAR_EWRITE:
        raise UnrarException("Write error")
    # res == SUCCESS | res == ERAR_END_ARCHIVE
    return res


def _check_close_result(res, func, args):
    if res == constants.ERAR_ECLOSE:
        raise UnrarException("Archive close error")
    # res == SUCCESS
    return res


# Return library API version.
RARGetDllVersion = _c_func(unrarlib.RARGetDllVersion, ctypes.c_int, [])


# Open RAR archive and allocate memory structures (unicode)
RAROpenArchiveEx = _c_func(unrarlib.RAROpenArchiveEx, HANDLE,
                           [ctypes.POINTER(RAROpenArchiveDataEx)],
                           _check_open_result)


# Set a password to decrypt files.
RARSetPassword = _c_func(unrarlib.RARSetPassword, ctypes.c_int,
                         [HANDLE, ctypes.c_char_p])


# Read header of file in archive (unicode).
RARReadHeaderEx = _c_func(unrarlib.RARReadHeaderEx, ctypes.c_int,
                          [HANDLE, ctypes.POINTER(RARHeaderDataEx)],
                          _check_readheader_result)


# Performs action and moves the current position in the archive to
# the next file. Extract or test the current file from the archive
# opened in RAR_OM_EXTRACT mode. If the mode RAR_OM_LIST is set,
# then a call to this function will simply skip the archive position
# to the next file. (unicode)
RARProcessFile = _c_func(unrarlib.RARProcessFile, ctypes.c_int,
                         [HANDLE, ctypes.c_int, ctypes.c_char_p,
                          ctypes.c_char_p], _check_process_result)


# Close RAR archive and release allocated memory. It must be called when
# archive processing is finished, even if the archive processing was stopped
# due to an error.
RARCloseArchive = _c_func(unrarlib.RARCloseArchive, ctypes.c_int, [HANDLE],
                          _check_close_result)
