/**
 * UGENE - Integrated Bioinformatics Tools.
 * Copyright (C) 2008-2017 UniPro <ugene@unipro.ru>
 * http://ugene.net
 *
 * 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */

#include <Python.h>
#include <structmember.h>

#include <U2Script/U2Script.h>

#define BASE_NAME_LENGTH                50

const int FUNCTION_SUCCESS =            0;
const int FUNCTION_FAIL =               -1;

/*
 * Check whether the `error` value is not U2_OK, and raise an appropriate exception
 *
 * Possible returning values:
 * FUNCTION_SUCCESS -       U2_OK == `error`
 * FUNCTION_FAIL -          otherwise
 *
 */

static int checkErrorCode( U2ErrorType error ) {
    PyObject *exceptionObject = NULL;
    PyUnicodeObject *exceptionString = NULL;
    const wchar_t *errorString = NULL;
    Py_ssize_t errorStringLength = 0;
    if ( U2_OK == error ) {
        return FUNCTION_SUCCESS;
    }
    switch ( error ) {
    case U2_FAILED_TO_CREATE_FILE :
    case U2_FAILED_TO_CREATE_DIR :
    case U2_FAILED_TO_READ_FILE :
    case U2_FAILED_TO_REMOVE_TMP_FILE :
        exceptionObject = PyExc_IOError;
        break;
    case  U2_INVALID_CALL :
        exceptionObject = PyExc_EnvironmentError;
        break;
    case U2_INVALID_NAME :
    case U2_INVALID_PATH :
    case U2_INVALID_STRING :
    case U2_NUM_ARG_OUT_OF_RANGE :
        exceptionObject = PyExc_ValueError;
        break;
    case U2_TOO_SMALL_BUFFER :
        exceptionObject = PyExc_BufferError;
        break;
    case U2_NOT_ENOUGH_MEMORY :
        exceptionObject = PyExc_MemoryError;
        break;
    default:
        exceptionObject = PyExc_Exception;
    }
    errorString = getErrorString( error );
    errorStringLength = ( Py_ssize_t )wcslen( errorString );
    exceptionString = ( PyUnicodeObject * )PyUnicode_FromWideChar( errorString, errorStringLength );
    PyErr_SetObject( exceptionObject, (PyObject *)exceptionString );
    return FUNCTION_FAIL;
}

/*
 * Release the memory acquired by the given C-string list.
 *
 * This function is supposed to be called only upon release of dynamically allocated array,
 * otherwise it causes memory violation error and program crash.
 *
 */

static void releaseStringList( int size, wchar_t **list ) {
    int index = 0;
    for ( ; index < size; ++index ) {
        free( list[index] );
    }
    free( list );
}

/*
 * Obtain last unsuccessful string value from UGENE environment
 * and build a Python object based on it
 *
 */

static PyObject * getLastFailedString( ) {
    const int initBufferSize = BASE_NAME_LENGTH * 2;
    int longNameSize = initBufferSize;
    wchar_t *longName = NULL;
    Py_ssize_t longNameLength = 0;
    U2ErrorType operationResult = U2_TOO_SMALL_BUFFER;
    PyObject *wrappedName = NULL;
    while ( U2_TOO_SMALL_BUFFER == operationResult ) {
        if ( initBufferSize != longNameSize ) {
            free( longName );
        }
        longName = ( wchar_t * )malloc( longNameSize * sizeof( wchar_t ) );
        if ( NULL == longName ) {
            checkErrorCode( U2_NOT_ENOUGH_MEMORY );
            return NULL;
        }
        operationResult = getLastFailedStringValue( longNameSize, longName, &longNameSize );
    }
    longNameLength = ( Py_ssize_t )wcslen( longName );
    wrappedName = PyUnicode_FromWideChar( longName, longNameLength );
    free( longName );
    return wrappedName;
}

/*
 * Wrap the `items` array of C-strings into a PyListObject instance
 *
 */

static PyListObject * convertToPyList( int itemsCount, const wchar_t **items ) {
    PyListObject *outputFilesList = ( PyListObject * )PyList_New( itemsCount );
    PyUnicodeObject *currentString = NULL;
    Py_ssize_t index = 0;

    for ( ; index < itemsCount; ++index ) {
        const Py_ssize_t itemLength = ( Py_ssize_t )wcslen( items[index] );
        currentString = ( PyUnicodeObject * )PyUnicode_FromWideChar( items[index], itemLength );
        if ( NULL == currentString ) {
            checkErrorCode( U2_NOT_ENOUGH_MEMORY );
            return NULL;
        }
        PyList_SetItem( ( PyObject * )outputFilesList, index, ( PyObject * )currentString );
    }
    return outputFilesList;
}

/*
 * Get Biopython's format name by U2Script's representation
 *
 */

static const char * getBioPythonFormatName( U2Format format ) {
    switch ( format ) {
    case U2_ABI :
        return "abi";
    case U2_ACE :
        return "ace";
    case U2_CLUSTAL :
        return "clustal";
    case U2_EMBL :
        return "embl";
    case U2_FASTA :
        return "fasta";
    case U2_FASTQ :
        return "fastq";
    case U2_GENBANK :
        return "genbank";
    case U2_MEGA :
        return "mega";
    case U2_MSF :
        return "msf";
    case U2_NEXUS :
        return "nexus";
    case U2_STOCKHOLM :
        return "stockholm";
    case U2_SWISS :
        return "swiss";
    default:
        return "";
    }
}

/*
 * Computational scheme class implementation
 *
 */

typedef struct {
    PyObject_HEAD
    SchemeHandle handle;
} Scheme;

/*
 * Perform the memory allocation and basic initialization for a scheme object
 *
 */

static PyObject * Scheme_alloc(PyTypeObject *type, PyObject *args, PyObject *kwds ) {
    Scheme *self = ( Scheme * )type->tp_alloc( type, 0 );
    kwds;
    if ( NULL != self ) {
        self->handle = NULL;
    }
    return ( PyObject * )self;
}

/*
 * Initialize the internal scheme representation
 *
 */

static int Scheme_init( PyObject *self, PyObject *args, PyObject *kwds ) {
    U2ErrorType result = U2_OK;
    const Py_UNICODE *pathToScheme = NULL, *inputFileForSas = NULL, *outputFileForSas = NULL;
    kwds;

    if ( 0 == PyArg_ParseTuple( args, "|uuu", &pathToScheme, &inputFileForSas,
        &outputFileForSas ) )
    {
        return -1;
    }
    result = createSas( ( const wchar_t * )pathToScheme, ( const wchar_t * )inputFileForSas,
        ( const wchar_t * )outputFileForSas, &( ( Scheme * )self )->handle );
    if ( U2_UNKNOWN_ELEMENT == result || U2_INVALID_STRING == result ) {
        result = createScheme( pathToScheme, &( ( Scheme * )self )->handle );
    }
    if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return -1;
    }
    return 0;
}

/*
 * Deallocate all the resources acquired by the computational scheme
 *
 */

static void Scheme_dealloc( Scheme *self ) {
    releaseScheme( self->handle );
    self->ob_type->tp_free( ( PyObject * )self );
}

/*
 * List of a Scheme instance fields
 *
 */

static PyMemberDef Scheme_members[] = { { NULL, 0, 0, 0, NULL } };

/*
 * Scheme class methods implementation
 *
 */

static PyObject * Scheme_addElement( PyObject* self, PyObject *args ) {
    U2ErrorType result = U2_OK;
    const Py_UNICODE *elementTypeName = NULL;
    Py_UNICODE elementName[BASE_NAME_LENGTH];

    if ( 0 == PyArg_ParseTuple( args, "u", &elementTypeName ) ) {
        return NULL;
    }
    result = addElementToScheme( ( ( Scheme * )self )->handle, ( const wchar_t * )elementTypeName,
        BASE_NAME_LENGTH, ( wchar_t * )elementName );

    if ( U2_TOO_SMALL_BUFFER == result ) {
        return getLastFailedString( );
    } else if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return NULL;
    }
    return Py_BuildValue( "u", elementName );
}

static PyObject * Scheme_addReader( PyObject* self, PyObject *args ) {
    U2ErrorType result = U2_OK;
    const Py_UNICODE *readerTypeName = NULL, *inputFilePath = NULL;
    Py_UNICODE readerName[BASE_NAME_LENGTH];

    if ( 0 == PyArg_ParseTuple( args, "uu", &readerTypeName, &inputFilePath ) ) {
        return NULL;
    }
    result = addReaderToScheme( ( ( Scheme * )self )->handle, ( const wchar_t * )readerTypeName,
        ( const wchar_t * )inputFilePath, BASE_NAME_LENGTH, ( wchar_t * )readerName );

    if ( U2_TOO_SMALL_BUFFER == result ) {
        return getLastFailedString( );
    } else if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return NULL;
    }
    return Py_BuildValue( "u", readerName );
}

static PyObject * Scheme_addWriter( PyObject* self, PyObject *args ) {
    U2ErrorType result = U2_OK;
    const Py_UNICODE *writerTypeName = NULL, *outputFilePath = NULL;
    Py_UNICODE writerName[BASE_NAME_LENGTH];

    if ( 0 == PyArg_ParseTuple( args, "uu", &writerTypeName, &outputFilePath ) ) {
        return NULL;
    }
    result = addWriterToScheme( ( ( Scheme * )self )->handle, ( const wchar_t * )writerTypeName,
        ( const wchar_t * )outputFilePath, BASE_NAME_LENGTH, ( wchar_t * )writerName );

    if ( U2_TOO_SMALL_BUFFER == result ) {
        return getLastFailedString( );
    } else if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return NULL;
    }
    return Py_BuildValue( "u", writerName );
}

static PyObject * Scheme_addActorsBinding( PyObject* self, PyObject *args ) {
    U2ErrorType result = U2_OK;
    const Py_UNICODE *srcElementName = NULL, *srcSlotName = NULL, *dstElementName = NULL,
        *dstPortAndSlotNames = NULL;

    if ( 0 == PyArg_ParseTuple( args, "uuuu", &srcElementName, &srcSlotName, &dstElementName,
        &dstPortAndSlotNames ) )
    {
        return NULL;
    }
    result = addSchemeActorsBinding( ( ( Scheme * )self )->handle,
        ( const wchar_t * )srcElementName, ( const wchar_t * )srcSlotName,
        ( const wchar_t * )dstElementName, ( const wchar_t * )dstPortAndSlotNames );

    if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return NULL;
    }
    Py_RETURN_NONE;
}

static PyObject * Scheme_addFlow( PyObject* self, PyObject *args ) {
    U2ErrorType result = U2_OK;
    const Py_UNICODE *srcElementName = NULL, *srcPortName = NULL, *dstElementName = NULL,
        *dstPortName = NULL;

    if ( 0 == PyArg_ParseTuple( args, "uuuu", &srcElementName, &srcPortName, &dstElementName,
        &dstPortName ) )
    {
        return NULL;
    }
    result = addFlowToScheme( ( ( Scheme * )self )->handle, ( const wchar_t * )srcElementName,
        ( const wchar_t * )srcPortName, ( const wchar_t * )dstElementName,
        ( const wchar_t * )dstPortName );

    if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return NULL;
    }
    Py_RETURN_NONE;
}

static PyObject * Scheme_setElementAttribute( PyObject* self, PyObject *args ) {
    U2ErrorType result = U2_OK;
    const Py_UNICODE *elementName = NULL, *attributeName = NULL, *attributeValue = NULL;

    if ( 0 == PyArg_ParseTuple( args, "uuu", &elementName, &attributeName, &attributeValue ) ) {
        return NULL;
    }

    result = setSchemeElementAttribute( ( ( Scheme * )self )->handle,
        ( const wchar_t * )elementName, ( const wchar_t * )attributeName,
        ( const wchar_t * )attributeValue );
    if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return NULL;
    }
    Py_RETURN_NONE;
}

static PyObject * Scheme_getElementAttribute( PyObject* self, PyObject *args ) {
    U2ErrorType result = U2_OK;
    const Py_UNICODE *elementName = NULL, *attributeName = NULL;
    Py_UNICODE attributeValue[BASE_NAME_LENGTH];

    if ( 0 == PyArg_ParseTuple( args, "uu", &elementName, &attributeName ) ) {
        return NULL;
    }
    result = getSchemeElementAttribute( ( ( Scheme * )self )->handle,
        ( const wchar_t * )elementName, ( const wchar_t * )attributeName,
        BASE_NAME_LENGTH, ( wchar_t * )attributeValue );
    if ( U2_TOO_SMALL_BUFFER == result ) {
        return getLastFailedString( );
    } else if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return NULL;
    }
    return Py_BuildValue( "s", attributeValue );
}

static PyObject * Scheme_launch( Scheme* self ) {
    PyListObject *outputFileList = NULL;
    wchar_t **outputFilePaths = NULL;
    int fileCount = 0;

    U2ErrorType result = launchScheme( self->handle, &fileCount, &outputFilePaths );
    if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return NULL;
    }
    outputFileList = convertToPyList( fileCount, outputFilePaths );
    releaseStringList( fileCount, outputFilePaths );
    return ( PyObject * )outputFileList;
}

static PyObject * Scheme_launchSas( PyObject* self, PyObject *args ) {
    PyListObject *outputFileList = NULL;
    wchar_t **outputFilePaths = NULL;
    int fileCount = 0;
    U2ErrorType result = U2_OK;
    const Py_UNICODE *algorithmName = NULL, *inputPath = NULL, *outputPath = NULL;

    if ( 0 == PyArg_ParseTuple( args, "uuu", &algorithmName, &inputPath, &outputPath ) ) {
        return NULL;
    }
    result = launchSas( ( const wchar_t * )algorithmName, ( const wchar_t * )inputPath,
        ( const wchar_t * )outputPath, &fileCount, &outputFilePaths );
    if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return NULL;
    }
    outputFileList = convertToPyList( fileCount, outputFilePaths );
    releaseStringList( fileCount, outputFilePaths );
    return ( PyObject * )outputFileList;
}

static PyObject * Scheme_saveToFile( PyObject* self, PyObject *args ) {
    U2ErrorType result = U2_OK;
    const Py_UNICODE *path = NULL;

    if ( 0 == PyArg_ParseTuple( args, "u", &path ) ) {
        return NULL;
    }
    result = saveSchemeToFile( ( ( Scheme * )self )->handle, ( const wchar_t * )path );
    if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return NULL;
    }
    Py_RETURN_NONE;
}

/*
 * List of the Scheme class methods
 *
 */

static PyMethodDef Scheme_methods[] = {
    { "addElement", ( PyCFunction )Scheme_addElement, METH_VARARGS,
        "Add to the computational scheme a new element of the supplied type"
        "and get its name" },
    { "addReader", ( PyCFunction )Scheme_addReader, METH_VARARGS,
        "Add to the computational scheme a new read element of the supplied type,"
        "set its input file path and get the element's name" },
    { "addWriter", ( PyCFunction )Scheme_addWriter, METH_VARARGS,
        "Add to the computational scheme a new write element of the supplied type,"
        "set its output file path and get the element's name" },
    { "addFlow", ( PyCFunction )Scheme_addFlow, METH_VARARGS,
        "Set binding between the given ports of the given computational elements." },
    { "addActorsBinding", ( PyCFunction )Scheme_addActorsBinding, METH_VARARGS,
        "Set binding between the given data slots of the given computational elements." },
    { "setElementAttribute", ( PyCFunction )Scheme_setElementAttribute, METH_VARARGS,
        "Specify the given attribute of the supplied element" },
    { "getElementAttribute", ( PyCFunction )Scheme_getElementAttribute, METH_VARARGS,
        "Get the value of the given element's attribute" },
    { "launch", ( PyCFunction )Scheme_launch, METH_NOARGS,
        "Run the scheme synchronously by the Workflow Designer" },
    { "saveToFile", ( PyCFunction )Scheme_saveToFile, METH_VARARGS,
        "Save the computational scheme to file in the UWL format to the given path" },
    { "launchSas", ( PyCFunction )Scheme_launchSas, METH_VARARGS | METH_STATIC,
        "Create and run the Single Algorithm Scheme synchronously by the Workflow Designer" },
    { NULL, NULL, 0, NULL }
};

/*
 * Scheme type definition
 *
 */

static PyTypeObject SchemeType = {
    PyObject_HEAD_INIT( NULL )
    0,                                          /*  ob_size*/
    "u2py_internals.Scheme",                    /*  tp_name*/
    sizeof( Scheme ),                           /*  tp_basicsize*/
    0,                                          /*  tp_itemsize*/
    ( destructor )Scheme_dealloc,               /*  tp_dealloc*/
    0,                                          /*  tp_print*/
    0,                                          /*  tp_getattr*/
    0,                                          /*  tp_setattr*/
    0,                                          /*  tp_compare*/
    0,                                          /*  tp_repr*/
    0,                                          /*  tp_as_number*/
    0,                                          /*  tp_as_sequence*/
    0,                                          /*  tp_as_mapping*/
    0,                                          /*  tp_hash */
    0,                                          /*  tp_call*/
    0,                                          /*  tp_str*/
    0,                                          /*  tp_getattro*/
    0,                                          /*  tp_setattro*/
    0,                                          /*  tp_as_buffer*/
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,   /*  tp_flags*/
    "Workflow Designer scheme objects",         /*  tp_doc */
    0,                                          /*  tp_traverse */
    0,                                          /*  tp_clear */
    0,                                          /*  tp_richcompare */
    0,                                          /*  tp_weaklistoffset */
    0,                                          /*  tp_iter */
    0,                                          /*  tp_iternext */
    Scheme_methods,                             /*  tp_methods */
    Scheme_members,                             /*  tp_members */
    0,                                          /*  tp_getset */
    0,                                          /*  tp_base */
    0,                                          /*  tp_dict */
    0,                                          /*  tp_descr_get */
    0,                                          /*  tp_descr_set */
    0,                                          /*  tp_dictoffset */
    ( initproc )Scheme_init,                    /*  tp_init */
    0,                                          /*  tp_alloc */
    Scheme_alloc                                /*  tp_new */
};

static PyObject * u2py_internals_initContext( PyObject *self, PyObject *args ) {
    const Py_UNICODE *workingDirectory = NULL;
    U2ErrorType result = U2_OK;

    if ( 0 == PyArg_ParseTuple( args, "u", &workingDirectory ) ) {
        return NULL;
    }
    result = initContext( ( const wchar_t * )workingDirectory );
    if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return NULL;
    }
    Py_RETURN_NONE;
}

static PyObject * u2py_internals_detectFormat( PyObject *self, PyObject *args ) {
    const Py_UNICODE *filePath = NULL;
    U2ErrorType result = U2_OK;
    U2Format format = U2_UNSUPPORTED;
    PyObject *formatName = NULL;

    if ( 0 == PyArg_ParseTuple( args, "u", &filePath ) ) {
        return NULL;
    }
    result = detectFileFormat( ( const wchar_t * )filePath, &format );
    if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return NULL;
    }
    return Py_BuildValue( "s", getBioPythonFormatName( format ) );
}

static PyObject * u2py_internals_releaseContext( PyObject *self, PyObject *args ) {
    U2ErrorType result = releaseContext( );
    if ( FUNCTION_SUCCESS != checkErrorCode( result ) ) {
        return NULL;
    }
    Py_RETURN_NONE;
}

static PyMethodDef u2py_internals_methods[] = {
    { "initContext", u2py_internals_initContext, METH_VARARGS,
        "Initialize UGENE environment, create all plugins and services" },
    { "detectFormat", u2py_internals_detectFormat, METH_VARARGS,
        "Get BioPython's format name for the given file" },
    { "releaseContext", u2py_internals_releaseContext, METH_NOARGS,
        "Deallocate all resources acquired by UGENE environment" },
    { NULL, NULL, 0, NULL }
};

/*
 * Initialize and register 'u2py_internals' module
 *
 */

PyMODINIT_FUNC initu2py_internals( ) {
    PyObject* mainModule = NULL;

    mainModule = Py_InitModule( "u2py_internals", u2py_internals_methods );
    if ( NULL == mainModule || 0 > PyType_Ready( &SchemeType ) ) {
        return;
    }
    Py_INCREF( &SchemeType );
    PyModule_AddObject( mainModule, "Scheme", ( PyObject * )&SchemeType );
}
