#!/usr/bin/env python3
# -*- coding: utf-8 -*-

""" 
Metadata Search Functionality for Tagged Settings Libraries.

**Project Name:**      MakeHuman

**Product Home Page:** http://www.makehumancommunity.org/

**Github Code Home Page:**    https://github.com/makehumancommunity/

**Authors:**           Marc Flerackers

**Copyright(c):**      MakeHuman Team 2001-2020

**Licensing:**         AGPL3

    This file is part of MakeHuman Community (www.makehumancommunity.org).

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero 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 Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.


Abstract
--------

This module implements a simple set of algorithms to handle MakeHuman 
metadata tags within large files. This is used to search for, load and 
save records in text files that are used to index library files. 
Each record in the text file contains a recordID that is the full file 
system path to a MakeHuman library file and a sequence of keywords 
attributed to that library file.  

This module has been tested with 30000 objects with 400000 tags, on an old PC,
with the following results:

- Load record using recordID, worst case:  0.00368905067444 sec
- Search records using tags, worst case:  0.0546329021454 sec
- Save/update records: 0.210602998734 sec

Because of these performance figures and because of MakeHuman file management requirements 
(single user, local files, basic usage, etc.), we have decided to use this baby-proof module 
to add new dependencies instead of using an ultra powerful external database that would be
of limited value in the context required for MakeHuman.

If the following content is contained in a plain text file called \"tags.txt\"...

::

    c:\MakeHuman\objectlibrary\obj1.obj hand long fist clenched
    c:\MakeHuman\objectlibrary\obj2.obj leg calf muscular
    c:\MakeHuman\objectlibrary\obj3.obj nose broad hooked
    c:\MakeHuman\objectlibrary\obj4.obj eye brown 
    c:\MakeHuman\objectlibrary\obj5.obj eyelash thick long
    c:\MakeHuman\objectlibrary\obj6.obj lips lush
    c:\MakeHuman\objectlibrary\obj7.obj sheyenne indian aboriginal
    ...

    the following examples illustrate the use of the load search and save functions:

    ::

    r1 = loadRecord('tags.txt', 'c:\MakeHuman\objectlibrary\obj2.obj')
    // Sets r1 to be the list [\"c:\MakeHuman\objectlibrary\obj2.obj\",\"leg\",\"calf\",\"muscular\"]

    r2 = searchRecord('tags.txt', 'eye')
    // Sets r2 to be the list [\"c:\MakeHuman\objectlibrary\obj4.obj\",\"c:\MakeHuman\objectlibrary\obj5.obj\",\"c:\MakeHuman\objectlibrary\obj7.obj\"]

    recordToSave = 'c:\MakeHuman\objectlibrary\obj5.obj eyelash thick long black'
    saveRecord('tags.txt', recordToSave)
    // Replaces the existing 'obj5' record with the new record in the file on disk.

"""

__docformat__ = 'restructuredtext'

import time
import log

def joinRecords(record1, record2):
    """
    This function takes two records and returns a single 
    concatenated record with the same record ID as the first.  
    The record is returned as a string containing the record ID at the start.
    
    Parameters
    ----------
   
    record1:     
      *list of strings*.  The first record.
      
    record2:     
      *list of strings*.  The records to append to the first record.
    """

    recordID = record1[0]
    fields1 = set(record1[1:])
    fields2 = set(record2[1:])
    joinedRecord = fields1.union(fields2)
    joinedRecord = list(joinedRecord)
    joinedRecord.insert(0, recordID)
    log.message('joining %s', joinedRecord)
    return ' '.join(joinedRecord)


def loadRecord(archivePath, recordID):
    """
    This function reads the file specified, searches for the specified record ID
    and returns that record if found or *None* if not found. 
    The record is returned as a list of strings containing the record ID in the '0' 
    element and successive fields in the following elements.
    
    Parameters
    ----------
   
    archivePath:     
      *string*.  The file system path to the file containing the set of records.
      
    recordID:     
      *string*.  The ID of the record to load.
    """

    time1 = time.perf_counter()
    with open(archivePath, 'r', encoding="utf-8") as f:
        record = None
        for line in f:
            if line.find(recordID) != -1:
                record = line.split()
                log.message('Found %s fields in %s sec', len(record), time.perf_counter() - time1)
                break
    return record


def searchRecord(archivePath, field):
    """
    This function reads the file specified, searches for the specified field
    and returns a list of the records that contain that field 
    (ie a list of strings containing recordIDs).
    
    Parameters
    ----------
   
    archivePath:     
      *string*.  The file system path to the file containing the set of records.
      
    field:     
      *string*.  The field to search for.
    """

    time1 = time.perf_counter()
    with open(archivePath, 'r', encoding="utf-8") as f:
        recordIDs = []
        for line in f:
            if line.find(field) != -1:
                recordIDs.append(line.split()[0])
    log.message('Found %s records in %s sec', len(recordIDs), time.perf_counter() - time1)
    return recordIDs


def saveRecord(archivePath, recordToSave):
    """
    This function searches a file to see if the specified record ID exists, 
    building and saving a consolidated record if it does and appending a new record to 
    the end of the file if it doesn't.
    
    Parameters
    ----------
   
    archivePath:     
      *string*.  The file system path to the file containing the set of records.
      
    recordToSave:     
      *string*.  The record to save.

      """

    time1 = time.perf_counter()
    recordID = recordToSave.split()[0]
    records = []
    isExistent = None
    try:
        with open(archivePath, 'w', encoding="utf-8") as f:
            i = 0
            for line in f:
                if line.find(recordID) != -1:
                    i += 1
                    isExistent = 1
                    oldRecord = line.split()
                    newRecord = recordToSave.split()
                    if oldRecord[0] == recordID:
                        line = joinRecords(newRecord, oldRecord)
                records.append(line.strip())
    except:
        log.message('A new %s archive will be created', archivePath)

    if not isExistent:
        records.append(recordToSave)

    with open(archivePath, 'w', encoding="utf-8") as f:
        for record in records:
            f.write('%s\n' % record)
    log.message('Record %s saved in %s sec', recordID, time.perf_counter() - time1)


