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

#
# Copyright (C) 2015 Yahoo Japan Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from ctypes import (Structure,
                    POINTER,
                    c_char,
                    c_uint,
                    c_uint8,
                    c_void_p,
                    c_char_p,
                    c_int,
                    c_double,
                    c_float,
                    cdll)
import ctypes.util
import csv


class NativeError(Exception):
    pass


class APIError(Exception):
    pass


class Index(object):
    '''
    NGT: Neighborhood Graph and Tree for Indexing High-dimensional Data
      NGT provides the functionality of searching for approximate nearest neighbors in high-dimensional data.

    Example:
    ```python
      from ngt import base as ngt
      import random

      dim = 10
      objects = []
      for i in range(0, 100) :
          vector = random.sample(range(100), dim)
          objects.append(vector)

      query = objects[0]
      index = ngt.Index.create(b"tmp", dim)
      index.insert(objects)
      # You can also insert objects from a file like this.
      # index.insert_from_tsv('list.dat')

      index.save()
      # You can load the saved index like this.
      # index = ngt.Index(b"tmp")

      result = index.search(query, 3)

      for i, o in enumerate(result) :
          print(str(i) + ": " + str(o.id) + ", " + str(o.distance))
          object = index.get_object(o.id)
          print(object)
    ```
    '''
    class ObjectDistance(Structure):
        _fields_ = [
            ('id', c_uint),
            ('distance', c_float)
        ]

        def __repr__(self):
            return "ObjectDistances({},{})".format(
                self.id,
                self.distance)

    __ngt = cdll.LoadLibrary(ctypes.util.find_library("ngt"))

    __ngt.ngt_open_index.argtypes = [c_char_p, c_void_p]
    __ngt.ngt_open_index.restype = c_void_p

    __ngt.ngt_create_graph_and_tree.argtypes = [c_char_p, c_void_p, c_void_p]
    __ngt.ngt_create_graph_and_tree.restype = c_void_p

    __ngt.ngt_create_property.argtypes = [c_void_p]
    __ngt.ngt_create_property.restype = c_void_p

    __ngt.ngt_save_index.argtypes = [c_void_p, c_char_p, c_void_p]

    __ngt.ngt_get_property.argtypes = [c_void_p, c_void_p, c_void_p]

    __ngt.ngt_get_property_dimension.argtypes = [c_void_p, c_void_p]

    __ngt.ngt_set_property_dimension.argtypes = [c_void_p, c_int, c_void_p]

    __ngt.ngt_set_property_edge_size_for_creation.argtypes = [
                                                c_void_p, c_int, c_void_p]

    __ngt.ngt_set_property_edge_size_for_search.argtypes = [
                                                c_void_p, c_int, c_void_p]

    __ngt.ngt_get_property_object_type.argtypes = [c_void_p]

    __ngt.ngt_is_property_object_type_float.argtypes = [c_int]

    __ngt.ngt_is_property_object_type_integer.argtypes = [c_int]

    __ngt.ngt_set_property_object_type_float.argtypes = [c_void_p, c_void_p]

    __ngt.ngt_set_property_object_type_integer.argtypes = [c_void_p, c_void_p]

    __ngt.ngt_set_property_distance_type_l1.argtypes = [c_void_p, c_void_p]

    __ngt.ngt_set_property_distance_type_l2.argtypes = [c_void_p, c_void_p]

    __ngt.ngt_set_property_distance_type_angle.argtypes = [c_void_p, c_void_p]

    __ngt.ngt_set_property_distance_type_hamming.argtypes = [c_void_p, c_void_p]

    __ngt.ngt_set_property_distance_type_jaccard.argtypes = [c_void_p, c_void_p]

    __ngt.ngt_set_property_distance_type_cosine.argtypes = [c_void_p, c_void_p]

    __ngt.ngt_create_empty_results.argtype = [c_void_p]
    __ngt.ngt_create_empty_results.restype = c_void_p

    __ngt.ngt_get_size.argtypes = [c_void_p, c_void_p]

    __ngt.ngt_get_result.argtypes = [c_void_p, c_int, c_void_p]
    __ngt.ngt_get_result.restype = ObjectDistance

    __ngt.ngt_create_index.argtypes = [c_void_p, c_int, c_void_p]

    __ngt.ngt_remove_index.argtypes = [c_void_p, c_uint, c_void_p]

    __ngt.ngt_get_object_space.argtype
s = [c_void_p]
    __ngt.ngt_get_object_space.restype = c_void_p

    __ngt.ngt_get_object_as_float.argtypes = [c_void_p, c_int, c_void_p]
    __ngt.ngt_get_object_as_float.restype = POINTER(c_float)

    
__ngt.ngt_get_object_as_integer.argtypes = [c_void_p, c_int, c_void_p]
    __ngt.ngt_get_object_as_integer.restype = POINTER(c_uint8)

    __ngt.ngt_destroy_results.argtypes = [c_void_p]

    __ngt.ngt_destroy_property.argtypes = [c_void_p]

    __ngt.ngt_close_index.argtypes = [c_void_p]

    __ngt.ngt_get_property_edge_size_for_creation.argtypes = [
                                                        c_void_p, c_void_p]

    __ngt.ngt_get_property_edge_size_for_search.argtypes = [c_void_p, c_void_p]

    __ngt.ngt_get_property_distance_type.argtypes = [c_void_p, c_void_p]

    __ngt.ngt_create_error_object.restype = c_void_p

    __ngt.ngt_get_error_string.argtypes = [c_void_p]
    __ngt.ngt_get_error_string.restype = c_char_p

    __ngt.ngt_clear_error_string.argtypes = [c_void_p]

    __ngt.ngt_destroy_error_object.argtypes = [c_void_p]

    @staticmethod
    def _check_error_num(status, error):
        if status == 0:
            message = Index.__ngt.ngt_get_error_string(error)
            Index.__ngt.ngt_clear_error_string(error)
            raise NativeError(message)

    @staticmethod
    def _check_error_pnum(status, error):
        if status < 0:
            message = Index.__ngt.ngt_get_error_string(error)
            Index.__ngt.ngt_clear_error_string(error)
            raise NativeError(message)

    @staticmethod
    def _check_error_obj(status, error):
        if status is None:
            message = Index.__ngt.ngt_get_error_string(error)
            Index.__ngt.ngt_clear_error_string(error)
            raise NativeError(message)

    @staticmethod
    def create(path, dimension,
               edge_size_for_creation=10, edge_size_for_search=40,
               object_type="Float", distance_type="L2"):
        '''
        create an empty index with the specified parameters.
          edge_size_for_creation : Number of edges for each node in the graph.
          edge_size_for_search   : Number of edges to search.
          object_type            : Type of the data object. (Float, Integer [Integer is 1 byte])
          distance_type          : Type of the distance function. (L1,L2,Angle,Hamming,Cosine)
        '''
        err = None
        prop = None
        index = None
        try:
            err = Index.__ngt.ngt_create_error_object()

            prop = Index.__ngt.ngt_create_property(err)
            Index._check_error_obj(prop, err)

            stat = Index.__ngt.ngt_set_property_dimension(prop, dimension, err)
            Index._check_error_num(stat, err)

            stat = Index.__ngt.ngt_set_property_edge_size_for_creation(
                                     prop, edge_size_for_creation, err)
            Index._check_error_num(stat, err)
            stat = Index.__ngt.ngt_set_property_edge_size_for_search(
                                     prop, edge_size_for_search, err)
            Index._check_error_num(stat, err)

            if object_type == "Float":
                stat = Index.__ngt.ngt_set_property_object_type_float(
                                                                prop, err)
                Index._check_error_num(stat, err)
            elif object_type == "Integer":
                stat = Index.__ngt.ngt_set_property_object_type_integer(
                                                                prop, err)
                Index._check_error_num(stat, err)

            if distance_type == "L1":
                stat