#!/usr/bin/python3
import ast,hashlib,os,random,re,secrets,shutil,subprocess,sys,tempfile
from uuid import uuid4
import sqlite3 as sql
from base64 import b64encode,b64decode
from Cryptodome.Cipher import AES

ROOT_DIR=os.getcwd() # The directory where all directories and files of the index are located
INDEX_FILE=ROOT_DIR+"/index.db" # The database file
LINUX_APP_STARTER="xdg-open" # The command which opens the files in the default application
ENCRYPT=True # True or False; Whether the default is to encrypt the file or to save it as a plain file
MAX_ITEMS=10 # Maximal amount of shown entries for selection

class database():
    def __init__(self,filepath = INDEX_FILE):
        self.connection=None
        self.crsr=None
        if not os.path.exists(filepath) :
            self.create_database(filepath)
        else:
            self.connection = sql.connect(filepath)
            self.crsr = self.connection.cursor()

    def add_index(self,vallist,collist=""):
        collist=self.collist if not collist else collist
        # compile the options into a command for the SQLite database
        colstring=",".join(collist)
        valstring='"{}"'.format('","'.join(vallist))
        self.crsr.execute("""INSERT INTO {table} ({cols})
                    VALUES ({vals});
                    """.format(table=self.name,cols=colstring,vals=valstring))
        self.connection.commit()

    def create_database(self, filepath):
        # create the database and tables
        self.connection = sql.connect(filepath)
        self.crsr = self.connection.cursor()
        sqlcommand = """CREATE TABLE FILES(
            FILE    TEXT PRIMARY KEY NOT NULL,
            HASH    TEXT  NOT NULL,
            TITLE   TEXT  ,
            SOURCE  TEXT  ,
            CATEGORY TEXT NOT NULL,
            TAGS    TEXT  ,
            CONTENT TEXT  
            ); """
        self.crsr.execute(sqlcommand)
        sqlcommand = """CREATE TABLE CATEGORY(
            NAME TEXT PRIMARY KEY NOT NULL,
            ALIAS    TEXT
            ); """
        self.crsr.execute(sqlcommand)
        sqlcommand = """CREATE TABLE TAGS(
            NAME TEXT PRIMARY KEY NOT NULL,
            ALIAS    TEXT
            ); """
        self.crsr.execute(sqlcommand)
        sqlcommand = """CREATE TABLE ENCRYPTION(
            NAME TEXT PRIMARY KEY NOT NULL,
            CIPHER TEXT NOT NULL,
            PASSWORD    TEXT
            ); """
        self.crsr.execute(sqlcommand)

    def delete_index(self,typ,item):
        self.crsr.execute("DELETE FROM {} WHERE {}='{}'".format(self.name,typ,item))
        self.connection.commit()
        return True

    def get_col(self,column = "*"):
        # get the column of some table. If no options given, return all columns
        self.crsr.execute("SELECT {} FROM {}".format(column,self.name))
        tres=self.crsr.fetchall()
        res=[]
        for i in tres:
            res.append(i)
        # if the table is empty, return "".
        if not res:
            res=""
        return res

    def get_item(self,column,where,specific=False):
        tres=[]
        if column == "*":
            for col in self.collist:
                temp_list=[]
                if specific:
                    self.crsr.execute("SELECT * FROM {} WHERE {}='{}'".format(self.name,col,where))
                else:
                    self.crsr.execute("SELECT * FROM {} WHERE {} GLOB '*{}*'".format(self.name,col,where))
                temp_list=self.crsr.fetchall()
                if temp_list:
                    for i in temp_list:
                        if not i in tres:
                            tres.append(i)
        else:
            if specific:
                self.crsr.execute("SELECT * FROM {} WHERE {}='{}'".format(self.name,column,where))
            else:
                self.crsr.execute("SELECT * FROM {} WHERE {} GLOB '*{}*'".format(self.name,column,where))
            tres=self.crsr.fetchall()
        n=0
        res=[]
        for i in tres:
            if not i in res:
                res.append(i)
        # if the table is empty, return "".
        if not res:
            return [""]
        return res

    def select_index(self,sel_list,quiet=False):
        if quiet == "strict":
            return sel_list
        if sel_list:
            res=[]
            if len(sel_list) > 1:
                n=0
                print("Found several matches:")
                for tup in sel_list[:MAX_ITEMS]:
                    temp_list=[]
                    for j in tup:
                        temp_list.append(j)
                    print("Match [{}]".format(n))
                    if self.name == "FILES":
                        print("\tTitle:\t ",temp_list[2])
                        name=ctb.get_alias(temp_list[4])
                        if name != "":
                            category=name
                        else:
                            category=temp_list[4]
                        print("\tCategory:",category)
                        tags_list=[]
                        for tag in temp_list[5].split(","):
                            name=ttb.get_alias(tag)
                            if name != "":
                                tag=name
                            tags_list.append(tag)
                        print("\tTags:\t ",",".join(tags_list))
                    else:
                        print("\tName:\t ",temp_list[0])
                        print("\tAlias:\t ",temp_list[1])
                        print("\tDescription: ",temp_list[2])
                    n+=1
                if len(sel_list) > MAX_ITEMS:
                    print(f"The list was too long, so it was reduced to {MAX_ITEMS} entries.")
                eingabe=input("Enter number(s) (0-{}; '*' for all entries): ".format(n-1))
                if not eingabe:
                    return [""]
                num_list=[]
                if re.match('[*]',eingabe):
                    for i in range(0,n):
                        num_list.append(i)
                else:
                    num_list=eingabe.split(' ')
                nminus=0
                for i in num_list:
                    if int(i) >= n:
                        print("The number {} is too big!".format(i))
                        nminus+=1
                        continue
                    res.append(sel_list[int(i)])
                if not quiet:
                    print("\nFinal match{}:".format("es" if len(num_list)-nminus > 1 else ""))
            else:
                if sel_list[0] == "":
                    if not quiet == "strict":
                        print("No matching entry found!")
                    return [""]
                res=sel_list
                if not quiet == "strict":
                    print("\nMatch found!")
            return res
        return 1    

    def sql_compare_list(self,typ,firstlist,secondlist,specific=False):
        if isinstance(firstlist, str):
            firstlist=firstlist.split(" ")
        if firstlist:
            n=0
            temp_list=[]
            if not secondlist:
                n=0
                for i in self.get_col(typ):
                    if len(temp_list) > MAX_ITEMS:
                        break
                    aliases=[]
                    if i == "" and self.name == "FILES":
                        print("NO ENTRIES IN THE INDEX!")
                        return ""
                    # get aliases for the checks, but only for unspecific search!
                    if self.name == "FILES":
                        if typ == "*":
                            for tag in i[5].split(","):
                                aliases.append(ttb.get_alias(tag))
                            aliases.append(ctb.get_alias(i[4]))
                        elif typ == "TAGS":
                            for tag in i[0].split(","):
                                aliases.append(ttb.get_alias(tag))
                        elif typ == "CATEGORY":
                            aliases.append(ctb.get_alias(i[0]))

                    istr=" ".join(i).lower() + " " + " ".join(aliases).lower()
                    success=0
                    for j in firstlist:
                        j=j.lower()
                        if specific:
                            for part in i:
                                part=part.lower()
                                if j == part:
                                    if not success == -1:
                                        success=1
                            if success == 0:
                                success=-1
                        else:
                            if j in istr:
                                if not success == -1:
                                    success=1
                            else:
                                success=-1
                    if success > 0:
                        if typ == "*":
                            temp_list.append(self.get_item("*",i[0],specific)[0])
                        else:
                            for k in self.get_item(typ,i[0],specific):
                                if not k in temp_list:
                                    temp_list.append(k)
                    n+=1

            else:
                if not secondlist[0] == "":
                    for i in secondlist:
                        for j in firstlist:
                            if j in i:
                                temp_list.append(secondlist[n])
                        n+=1
                else:
                    return secondlist
            if not temp_list:
                return [""]
            return temp_list[:MAX_ITEMS+1]
        return secondlist

    def update_index(self,typ,update,where,val):
        self.crsr.execute("UPDATE {} SET {}='{}' WHERE {}='{}'".format(self.name,typ,update,where,val))
        self.connection.commit()
        return True

class enctable(database): # https://www.thesecuritybuddy.com/cryptography-and-python/aes-encryption-and-decryption-using-pycryptodome-module-in-python/
    def __init__(self, filepath = INDEX_FILE):
        self.name="ENCRYPTION"
        self.collist=["NAME","CIPHER","PASSWORD"]
        super().__init__(filepath)

    def derive_key_and_iv(self, password, salt, key_length, iv_length): # derive key and IV from password and salt.
        d = d_i = b''
        while len(d) < key_length + iv_length:
            d_i = hashlib.md5(d_i + password + salt).digest() # obtain the md5 hash value
            d += d_i
        return d[:key_length], d[key_length:key_length+iv_length]

    def encrypt(self, in_file, out_filepath, password="", key_length=32):
        print("Encrypting...")
        in_file=open(in_file,"rb")
        out_uuid=out_filepath.split("/")[-1].split(".")[0]
        out_file=open(f"{out_filepath}","wb")
        bs = AES.block_size # 16 bytes
        if not password:
            password = os.urandom(bs*random.randint(1,4))
            if self.get_item("NAME", out_uuid)[0] == "":
                self.add_index([out_uuid.split(".")[0],"AES",b64encode(password).decode()])
            else:
                password_list=self.get_item("NAME", in_uuid.split(".")[0])
                if password_list[0] != "":
                    if len(password_list) == 1:
                        password=b64decode(password_list[0][2].encode())
                    else:
                        print("ERROR: MULTIPLE PASSWORD ENTRIES FOUND!")
                        return False
        salt = os.urandom(bs) # return a string of random bytes
        key, iv = self.derive_key_and_iv(password, salt, key_length, bs)
        cipher = AES.new(key, AES.MODE_CBC, iv)
        out_file.write(salt)
        finished = False

        while not finished:
            chunk = in_file.read(1024 * bs) 
            if len(chunk) == 0 or len(chunk) % bs != 0:# final block/chunk is padded before encryption
                padding_length = (bs - len(chunk) % bs) or bs
                chunk += str.encode(padding_length * chr(padding_length))
                finished = True
            out_file.write(cipher.encrypt(chunk))
        out_file.close()
        in_file.close()

    def is_encrypted(self, uuid):
        if self.get_item("NAME", uuid)[0] != "":
            return True
        return False

    def decrypt(self, in_filepath, out_file=None, password="", key_length=32):
        print("Decrypting...")
        in_file=open(f"{in_filepath}","rb") # open the encrypted file
        in_uuid=in_filepath.split("/")[-1]
        if not out_file:
            out_temp=tempfile.mkstemp(prefix="image-index-")
            filepath=out_temp[1]
            out_file=open(filepath,"wb")
        else:
            filepath=out_file
            out_file=open(out_file,"wb")
        if not password:
            password_list=self.get_item("NAME", in_uuid.split(".")[0])
            if password_list[0] != "":
                if len(password_list) == 1:
                    password=b64decode(password_list[0][2].encode())
                elif len(password_list) > 1:
                    print("ERROR: MULTIPLE PASSWORD ENTRIES FOUND!")
                    return False
            else:
                print("ERROR: NO PASSWORD FOUND FOR DECRYPTION!")
                return False
        else:
            password=b64decode(password.encode())
        bs = AES.block_size
        salt = in_file.read(bs)
        key, iv = self.derive_key_and_iv(password, salt, key_length, bs)
        cipher = AES.new(key, AES.MODE_CBC, iv)
        next_chunk = ''
        finished = False
        while not finished:
            chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs))
            if len(next_chunk) == 0:
                padding_length = chunk[-1]
                chunk = chunk[:-padding_length]
                finished = True 
            out_file.write(bytes(x for x in chunk)) 
        out_file.close()
        in_file.close()
        return filepath

    def delete_index(self, uuid):
        super().delete_index("NAME", uuid)
        print("UUID",uuid)
        return True

class metatable(database):
    def __init__(self,typ,filepath = INDEX_FILE):
        self.name=typ.upper()
        self.collist=["NAME","ALIAS"]
        super().__init__(filepath)

    def add_index(self,val,alias,randhex=None):
        if not randhex:
            randhex=get_randchar()
        if self.name == "TAGS" or bencrypt:
            val=get_randchar(8)
        else:
            val=re.sub('[ ,?!/\\:!*"<>|]', '', val)
        super().add_index([val[:8] + "-" + randhex,alias])

    def check_index(self,typ):
        res=[]
        for i in tb.get_col(typ):
            success=0
            for j in self.get_col("NAME"):
                if j[0].split(".")[0] in i[0]:
                    success=1
            if not i[0] in res and success == 0:
                res.append(i[0])
        return res

    def get_alias(self,arg):
        if not arg:
            return ""
        selection=self.search_index(arg,"strict")
        item=selection[0]
        if item[0] != "":
            alias=item[1]
        else:
            alias = ""
        return alias

    def get_name(self,arg):
        if not arg:
            return ""
        selection=self.search_index(arg,"strict")
        item=selection[0]
        if not item:
            name=""
        else:
            name=item[0]
        return name

    def search_index(self,args,quiet=True):
        selection=[]
        selection=self.sql_compare_list("*", [args], selection,True)
        if selection[0] == "":
            slist=args.split(" ")
            selection=self.sql_compare_list("*", slist, [],False)
        if len(selection) > 1:
            print("Please enter a more specific search query!")
            return ""
        selection=self.select_index(selection,quiet)
        return selection

    def update_index(self, typ, update, where, val):
        selection=self.search_index(update,"strict")
        if selection[0] != "" and len(selection) >= 1:
            print("One entry is already called {}!".format(update))
            return False
        else:
            super().update_index(typ, update, where, val)
            return True

class filestable(database):
    def __init__(self,filepath = INDEX_FILE):
        self.name="FILES"
        self.collist=["FILE","HASH","TITLE","SOURCE","CATEGORY","TAGS","CONTENT"]
        super().__init__(filepath) 
    
    def add_index(self,filepath,category="default",title="",source="",tags="",content=""):
        filehash=self.get_hash(filepath) # make hash of file before copy
        if filehash in str(self.get_col("HASH")):
            print("This file already has an entry!")
            return False
        n=0
        # get the name of the category from the meta table
        if not category:
            category="default"
        name=ctb.get_name(category)
        if name:
            category=name
        else:
            ctb.add_index(category.lower(),category)
            category=ctb.get_name(category)

        # get the name of the tags from the meta table
        tags_list=[]
        for tag in tags.split(','):
            name=ttb.get_name(tag)
            if name != "":
                tag=name
            else:
                ttb.add_index(tag.lower(),tag)
            tags_list.append(ttb.get_name(tag))
        tags=",".join(tags_list)

        fileext=os.path.splitext(filepath)[-1]
        fileuuid=str(uuid4())
        filename=fileuuid+fileext
        if not os.path.exists("{}/{}".format(ROOT_DIR,category)):
            os.makedirs("{}/{}".format(ROOT_DIR,category))
        # try to copy the file, return if error.
        try:
            if bencrypt:
                etb.encrypt(filepath, f"{ROOT_DIR}/{category}/{fileuuid}.enc")
            else:
                shutil.copy(filepath,f"{ROOT_DIR}/{category}/{filename}")
        except Exception as e:
            print(e)
            print("COULDN'T COPY FILE TO DESTINATION!")
            return False
        vallist=[filename,filehash,title,source,category,tags,content]
        super().add_index(vallist)  
        return True
    
    def check_index(self):
        hash_list=[]
        path_list=[]
        enc_num=0
        for i in self.get_col():
            filename=i[0]
            fileuuid=filename.split(".")[0]
            category=i[4]
            filehash1=i[1]
            if etb.is_encrypted(fileuuid):
                enc_num+=1
                filename=f"{fileuuid}.enc"
            filepath="{}/{}/{}".format(ROOT_DIR,category,filename)
            if not os.path.exists(filepath):
                path_list.append(i)
            if etb.is_encrypted(fileuuid):
                continue
            try:
                filehash2=self.get_hash(filepath)
                if not filehash1 == filehash2:
                    hash_list.append(i)
            except Exception:
                path_list.append(i)
        print(f"Encrypted files: {enc_num}; hashes not checked.")
        return hash_list,path_list
    
    def delete_index(self,sel_list):
        item_list=self.get_item(self.collist[1],sel_list[1])
        if len(item_list) == 1:
            item=item_list[0]
        category=item[4]
        filename=item[0]
        fileuuid=os.path.splitext(filename)[0]
        super().delete_index(self.collist[1],sel_list[1])
        etb.delete_index(fileuuid)
        return True

    def replace_file(self,in_filepath,item):
        filehash=self.get_hash(in_filepath)
        category=item[4]
        filename=item[0]
        if filehash in str(self.get_col("HASH")):
            print("This file already has an entry!")
            return False
        fileuuid=os.path.splitext(filename)[-1]
        out_filepath=f"{ROOT_DIR}/{category}/{filename}"
        out_encpath=f"{ROOT_DIR}/{category}/{fileuuid}.enc"
        try:
            if etb.is_encrypted(fileuuid):
                etb.encrypt(in_filepath, out_encpath)
            else:
                shutil.copy(in_filepath, out_filepath)
        except Exception as e:
            print("ERROR:",e)
            return False
        super().update_index("HASH", filehash, self.collist[0], filename)
        return True

    def update_index(self,typ,update,sel_list,omnipotent=False):
        typ=typ.upper()
        if typ in ["FILE","HASH"] and not omnipotent:
            print("This type can't be changed!")
            return False
        category=sel_list[4]
        filehash=sel_list[1]
        filename=sel_list[0]
        fileuuid=sel_list[0].split(".")[0]
        if typ in ["CATEGORY"]:
            # get alias of category
            name=ctb.get_name(update)
            if name != "":
                update=name
            else:
                ctb.add_index(update.lower(),update)
                update=ctb.get_name(update)
            if not os.path.exists("{}/{}".format(ROOT_DIR,update)):
                os.makedirs("{}/{}".format(ROOT_DIR,update))
            if etb.is_encrypted(fileuuid):
                filename=f"{fileuuid}.enc"
            shutil.move("{}/{}/{}".format(ROOT_DIR,category,filename), "{}/{}/{}".format(ROOT_DIR,update,filename))
        if typ in ["TAGS"]:
            tags_list=[]
            if update[0][0] == "+":
                for tag in sel_list[5].split(","):
                    tags_list.append(tag)
                for tag in update[1:].split(","):
                    name=ttb.get_name(tag)
                    if name == "":
                        ttb.add_index(tag.lower(), tag)
                        name=ttb.get_name(tag)
                    tags_list.append(name)
            elif update[0][0] == "-":
                for tag in sel_list[5].split(","):
                    success=0
                    for i in update[1:].split(","):
                        name=ttb.get_name(i)
                        if name == "":
                            ttb.add_index(tag.lower(), tag)
                            name=ttb.get_name(tag)
                        if name == tag:
                            success = -1
                    if success >= 0:
                        tags_list.append(tag)
            else:
                for tag in update.split(","):
                    name=ttb.get_name(tag)
                    if name != "":
                        tags_list.append(name)
                    else:
                        ttb.add_index(tag.lower(), tag)
                        tags_list.append(ttb.get_name(tag))
            update=",".join(tags_list)
        super().update_index(typ, update, "HASH", filehash)
    
    def get_hash(self,filepath):
        # https://www.quickprogrammingtips.com/python/how-to-calculate-md5-hash-of-a-file-in-python.html
        md5_hash = hashlib.md5()
        # hash selected file in chunks of 4KiB, read the link above if you ask why.
        with open(filepath,"rb") as f:
            for byte_block in iter(lambda: f.read(4096),b""):
                md5_hash.update(byte_block)
            f.close()
            return str(md5_hash.hexdigest())
    
    def search_index(self,args,quiet=False):
        snext="all"
        shash=[]
        alle=[]
        category=[]
        sfile=[]
        content=[]
        source=[]
        title=[]
        tags=[]
        for arg in args:
            arg=arg.lower()
            if re.match('^[-]\w{1}$', arg):
                if arg == "-h":
                    snext="hash"
                elif arg == "-a": # technically unneeded because of else at the end
                    snext="all"
                elif arg == "-c":
                    snext="category"
                elif arg == "-f":
                    snext="file"
                elif arg == "-i": # Inhalt
                    snext="content"
                elif arg == "-s":
                    snext="source"
                elif arg == "-t":
                    snext="title"
                elif arg == "-g": # Gruppe 
                    snext="tags"
                else:
                    snext="all"
                continue
            if snext == "hash":
                shash.append(arg)
            elif snext == "all":
                alle.append(arg)
            elif snext == "category":
                name=ctb.get_name(arg)
                if name != "":
                    arg=name
                category.append(arg)
            elif snext == "file":
                sfile.append(arg)
            elif snext == "content":
                content.append(arg)
            elif snext == "source":
                source.append(arg)
            elif snext == "title":
                title.append(arg)
            elif snext == "tags":
                '''if "," in arg:
                    for tag in arg.split(","):
                        name=ttb.get_name(arg)
                        if name != "":
                            arg=name
                        tags.append(arg)
                else:
                    name=ttb.get_name(arg)
                    print("name",name)
                    if name != "":
                        arg=name'''
                tags.append(arg)
            else:
                alle.append(arg)
        # search for the right items
        selection=[]
        selection=self.sql_compare_list("*",alle,selection)
        selection=self.sql_compare_list("HASH",shash,selection)
        selection=self.sql_compare_list("CATEGORY",category,selection)
        selection=self.sql_compare_list("FILE",sfile,selection)
        selection=self.sql_compare_list("CONTENT",content,selection)
        selection=self.sql_compare_list("SOURCE",source,selection)
        selection=self.sql_compare_list("TITLE",title,selection)
        selection=self.sql_compare_list("TAGS",tags,selection)
        
        return self.select_index(selection,quiet)

def get_randchar(count=5):
    randhex=""
    for i in range(count):
        randhex+=random.choice("0123456789abcdefghijklmnopqrstuvwxyz")
    return randhex

def add(args):
    if len(args) >= 6:
        tb.add_index(args[0],args[1],args[2],args[3],args[4],args[5])
        return
    n=0
    for i in ["Filepath","Category","Title","Source","Tags","Content"]:
        i=i.title()
        try:
            print("{}: {}".format(i,args[n]))
        except Exception:
            extra=""
            if i == "Tags":
                extra=" (Separate with ',')"
            eingabe = input("{}{}: ".format(i,extra))
            if i in ["Category"] and not eingabe:
                print("{} set to 'default'".format(i))
                eingabe="default"
            args.append(eingabe)
        if i in ["Filepath"]:
            filehash=tb.get_hash(args[n])
            if filehash in str(tb.get_col("HASH")):
                print("This file already exists!")
                return False
            if not args[n]:
                print("{} must not be empty!".format(i))
                return False
            else:
                if not os.path.isfile(args[n]):
                    print(" The file '{}' doesn't exist or is not a file!".format(args[n]))
                    return False
        n+=1
    success=tb.add_index(args[0],args[1],args[2],args[3],args[4],args[5])
    if success:
        print("Added '{}'!".format(args[2]))

def copy(args):
    if len(args) == 0:
        out_filepath=input()
        sel_list=search([])
    elif len(args) == 1:
        out_filepath=args[0]
        sel_list=search([])
    else:
        out_filepath=args[0]
        sel_list=search(args[1:])
    for item in sel_list:
        filename=item[0]
        out_fileext=os.path.splitext(filename)[-1]
        title=item[2]
        category=item[4]
        fileuuid=os.path.splitext(filename)[0]
        extra=""
        if os.path.exists(out_filepath):
            if os.path.isfile(out_filepath):
                if re.match('[nN].*', input(f"The file on path {out_filepath} already exists!\nDo you want to overwrite it? [Y/n] ")):
                    return False
            if os.path.isdir(out_filepath):
                print("found DIRECTORY")
                extra=f"/{title}{out_fileext}"
        if etb.is_encrypted(fileuuid):
            etb.decrypt(f"{ROOT_DIR}/{category}/{fileuuid}.enc",f"{out_filepath}{extra}")
        else:
            shutil.copy(f"{ROOT_DIR}/{category}/{filename}", f"{out_filepath}{extra}")
        print(f"Copied '{title}' to {out_filepath}!")

def check(args):
    success=0
    hash_list,temp_list=tb.check_index()
    path_list=[]
    hashcheck=pathcheck=True
    verbose=False
    for arg in args:
        if arg == "-v":
            verbose=True
        elif arg == "-f":
            hashcheck=False
        elif arg == "-h":
            pathcheck=False

    if hash_list:
        print("{} file{} faulty!".format(len(hash_list),"s are" if len(hash_list) > 1 else " is"))
        success=-1
        if verbose:
            for tup in hash_list:
                print("Title: ",tup[2])
                print("\tCategory:",tup[4])
                print("\tFilename:",tup[0])
    for i in temp_list:
        if not i in path_list:
            path_list.append(i)
    if path_list:
        print("{} file{} missing!".format(len(path_list),"s are" if len(path_list) > 1 else " is"))
        success=-1
        if verbose:
            for tup in path_list:
                print("Title: ",tup[2])
                print("\tCategory:",tup[4])
                print("\tFilename:",tup[0])
    if success >= 0:
        print("Everything is good!")
    if hash_list and hashcheck:
        eingabe=input("Do you want to remove the faulty files? [y/N]: ")
        if re.match('[yY]',eingabe):
            print("Removing faulty files...")
            repair(hash_list)
    if path_list and pathcheck:
        eingabe=input("Do you want to remove the orphaned entries? [Y/n]: ")
        if not re.match('[nN]',eingabe):
            print("Removing orphaned entries...")
            repair(path_list)

def delete(args):
    selection=search(args,True)
    for sel in selection:
        if sel[0] != "":
            try:
                category=sel[4]
                filename=sel[0]
                fileuuid=os.path.splitext(filename)[0]
                if etb.is_encrypted(fileuuid):
                    filename=f"{fileuuid}.enc"
                os.remove("{}/{}/{}".format(ROOT_DIR,category,filename))
            except Exception as e:
                print(e)
                print("Couldn't delete a file!")
                return 1
            tb.delete_index(sel)
            print("Deleted '{}'!".format(sel[2]))

def help(args):
    syntax=False
    for arg in args:
        if not syntax and not re.match('[mM]',arg):
            print("SYNTAX: image-index <option> [args]")
            syntax=True
        if re.match('[aA].*',arg):
            print("add:\tadds a new entry;\n\tInstant: image-index add <filepath> <category> <title> <source> <tags> <content>")
            print("\tPrompt:  image-index add")
            print("Encryption: -e: Encrypt the added file\n\t    -p: Do not encrypt the added file (plain)")
            print('EXAMPLES:\nimage-index add ~/Pictures/example.jpg "A Category" "Example file" "https://example.org" "Tag,Example,Some thing" "This is an example for the add option."')
            print("image-index add ~/Videos/movie.mp4 (This will ask for the other options in a prompt)\n")
        elif re.match('[mM].*',arg):
            meta_help()
        elif re.match('[cC].*', arg):
            print("copy:\tcopies a file to a custom filepath based on a search query;\n\tInstant: image-index copy <filepath> <words/filters>")
            print("\tPrompt:  image-index copy")
            print("EXAMPLE:\nimage-index copy ~/Pictures/Example.jpg -t example")
            print("image-index copy ~/Pictures/ -t example (creates a file with the title and extension of the entry)\n")
        elif re.match('[cC][hH].*',arg) or re.match('[cC].*[eE].*',arg):
            print("check:\tchecks the existence and correctness of all files in the index;\n\tSyntax: image-index check [options]")
            print("\tOptions: -v: show every faulty/orphaned entry")
            print("\t\t -f: check only if files exist (disables the other check)")
            print("\t\t -h: check only if hashes are correct (disables the other check)\n")
        elif re.match('[dD].*',arg):
            print("delete:\tdeletes a file and entry based on a search query;\n\tInstant: image-index delete <words/filters>")
            print("\tPrompt:  image-index delete")
            print("EXAMPLE:\nimage-index delete -t Example -g Tag Example -a .mp4 add\n")
        elif re.match('[iI].*',arg):
            print("\timport:\tshows an add prompt for every file in a directory;\n\t\tSyntax: image-index import <directory path>")
            print("EXAMPLE:\nimage-index import ~/Pictures/Vacation\n")
        elif re.match('[oO].*',arg):
            print("open:\topens a file based on a search query in the standard app;\n\tInstant: image-index open <words/filters>")
            print("\tPrompt:  image-index open")
            print("EXAMPLE:\nimage-index open example -s example.org -i an example\n")
        elif re.match('[rR].*',arg):
            print("replaces a file of an entry based on a search query;\n\tInstant: image-index replace <replacement_file> <words/filters>")
            print("\tPrompt:  image-index replace")
            print("EXAMPLE:\nimage-index replace ~/Pictures/example_new.jpg -f .jpg\n")
        elif re.match('[sS][tT].*',arg) or re.match('[sS].*[aA].*',arg):
            print("stats:\tshows some statistics about the index;\n\tSyntax: image-index stats [-v]")
            print("EXAMPLE:\nimage-index stats -v (also shows all tags and categories)\n")
        elif re.match('[sS].*',arg):
            print("show:\tsearches through the index and shows the matches;\n\tInstant: image-index show <words/filters>")
            print("\tPrompt:  image-index show")
            print("FILTERS: -a: All types\n\t -c: Category\n\t -f: Filename\n\t -g: Tags")
            print("\t -h: Hash\n\t -i: Content\n\t -s: Source\n\t -t: Title")
            print("EXAMPLE:\nimage-index show example -c category\n")
        elif re.match('[uU].*',arg):
            print("update:\tchanges specific column based on a search query;\n\tInstant: image-index update <column> <updated_value> <words/filters>")
            print("\tPrompt:  image-index update")
            print('EXAMPLES:\nimage-index update category "New Category" -t example')
            print('image-index update Tags "Tag,Example,Test" -s https:// -a example')
            print('image-index update title "Some new title" -g "some thing" -a an example')
            print('SPECIAL EXAMPLES TAGS:\nimage-index update tags +New_tag,hello -g "some thing" (adds the tags "New_tag" and "hello")')
            print('image-index update tags -Tag -t Example (removes the tag "Tag")\n')
    if not args:
            print("SYNTAX: image-index <option> [args]")
            print("OPTIONS:\n\thelp:\tdisplays helpful text\}n\t\tSyntax: image-index help [commands]")
            print("\tmeta:\tdisplays help for the metadata tables")
            print("\tadd:\tadds a new entry;\n\t\tSyntax: image-index add <filepath> <category> <title> <source> <tags> <content>")
            print("\tcheck:\tchecks the existence and correctness of all files in the index;\n\t\tSyntax: image-index check [options]")
            print("\tcopy:\tcopies a file to a custom filepath based on a search query;\n\t\tSyntax: image-index copy <filepath> <words/filters>")
            print("\tdelete:\tdeletes a file and entry based on a search query;\n\t\tSyntax: image-index delete <words/filters>")
            print("\timport:\tshows an add prompt for every file in a directory;\n\t\tSyntax: image-index import <directory path>")
            print("\topen:\topens a file based on a search query in the standard app;\n\t\tSyntax: image-index open <words/filters>")
            print("\treplace:replaces a file of an entry based on a search query;\n\t\tSyntax: image-index replace <file> <words/filters>")
            print("\tshow:\tsearches through the index and shows the matches;\n\t\tSyntax: image-index show <words/filters>")
            print("\tstats:\tshows some stats about the index;\n\t\tSyntax: image-index stats [-v]")
            print("\tupdate:\tchanges specific column based on a search query;\n\t\tSyntax: image-index update <column> <updated_value> <words/filters>")

def imports(args):
    for arg in args:
        if arg[-1] != "/":
            arg+="/"
        if os.path.exists(arg):
            if not os.path.isfile(arg):
                for sfile in os.listdir(arg):
                    if os.path.isfile(arg+sfile):
                        add(["{}{}".format(arg,sfile)])
            else:
                print("Path '{}' is a file!".format(arg))
        else:
            print("Path '{}' doesn't exist!".format(arg))

def meta(args):
    if len(args) == 0 or re.match('[hH].*',args[0]):
        meta_help()
    else:
        if len(args) <= 1:
            print("Options: Category, Tags")
            args.append(input("Input one type to check from the list above: "))
        command=args[0]
        typ=args[1]
        args=args[2:]
        if re.match('[cC].*',command):
            meta_check(typ,args)
        elif re.match('[uU].*',command):
            meta_update(typ,args)

def meta_check(typ,args):
    if re.match('[cC].*',typ):
        tres=ctb.check_index("CATEGORY")
    elif re.match('[tT].*',typ):
        tres=ttb.check_index("TAGS")
    else:
        print("No valid type specified!")
        return False
    if tres:
        yas=True
        print("Missing items:",','.join(tres))
        eingabe=input("Do you want to add aliases to them?[Y/n]: ")
        if re.match('[nN]',eingabe):
            yas=False
            print("yo")
        for val in tres:
            if yas:
                eingabe=input("Enter Alias for {}: ".format(val.upper()))
            else:
                eingabe=""
            if re.match('[cC].*',typ):
                ctb.add_index(val,eingabe)
            elif re.match('[tT].*',typ):
                ttb.add_index(val,eingabe)
    else:
        print("Everything is good!")

def meta_update(typ,args):
    if len(args) == 0:
        args.append(input("Enter the alias to update: "))
        args.append(input("Enter the updated alias: "))
    elif len(args) == 1:
        args.append(input("Enter the updated alias: "))
    search=args[0]
    update=args[1]
    if re.match('[cC].*',typ):
        sel_list=ctb.search_index(search)
        for item in sel_list:
            alias=ctb.get_alias(item[0])
            success=ctb.update_index("ALIAS", update, "NAME", item[0])
    elif re.match('[tT].*',typ):
        sel_list=ttb.search_index(search)
        for item in sel_list:
            alias=ttb.get_alias(item[0])
            success=ttb.update_index("ALIAS", update, "NAME", item[0])
    else:
        print("The first argument needs to be either 'Category' or 'Tags'!")
        return False
    if item[0] != "" and success:
        print("Updated {} to {}".format(alias,update))

def meta_help():
    print("SYNTAX:  image-index meta <option> [args]")
    print("OPTIONS:\n\thelp:\tdisplays this text")
    print("\tcheck:\tcheck which items don't have an entry yet;\n\t\tSyntax: image-index meta check <Category/Tags>")
    print("\tupdate:\tchange an alias of one entry based on a search query;\n\t\tSyntax: image-index update <Category/Tags> [entry] [alias]")
    print('\t\tExamples: image-index meta update tags Example "New alias"')
    print('\t\t\t  image-index meta update category "A Category" "Example Category"\n')

def opens(args):
    plat=sys.platform
    selection=search(args,True)
    for sel in selection:
        if not sel[0] == "":
            fileuuid=os.path.splitext(sel[0])[0]
            if etb.is_encrypted(fileuuid):
                filename=f"{fileuuid}.enc"
            else:
                filename=sel[0]
            category=sel[4]
            filepath="{}/{}/{}".format(ROOT_DIR,category,filename)
        else:
            continue
        if etb.is_encrypted(fileuuid):
            filepath=etb.decrypt(filepath) # temporary file in RAM
        if plat.startswith('linux'):
            subprocess.Popen([LINUX_APP_STARTER, filepath])
        else:
            os.startfile(filepath)

def update(args):
    n=0
    for i in ["column","updated string"]:
        try:
            trash=args[n]
        except Exception as e:
            eingabe=input("Enter {}: ".format(i))
            args.append(eingabe)
        n+=1
    selection=search(args[n:],True)
    typ=args[0]
    update=args[1]
    for sel in selection:
        if sel[0] != "":
            n=0
            for i in tb.collist:
                if i.lower() == typ.lower():
                    old=sel[n]
                else:
                    n+=1
            tb.update_index(typ,update,sel)
            if re.match('[tT][aA].*', typ) and re.match('[-+]', update[0]):
                tags_list=[]
                for tag in update[1:].split(","):
                    tags_list.append(tag)
                tagstr=", ".join(tags_list)
                if update[0] == "-":
                    print("Removed {}!".format(tagstr))
                elif update[0] == "+":
                    print("Added {}!".format(tagstr))
            else:
                print("Updated {} to {}!".format(old,update))

def repair(err_list):
    sel_list=[]
    for i in err_list:
        if not i in sel_list:
            sel_list.append(i)
    for tup in sel_list:
        filename=tup[0]
        category=tup[4]
        filepath="{}/{}/{}".format(ROOT_DIR,category,filename)
        if os.path.exists(filepath):
            os.remove(filepath)
        tb.delete_index(tup)

def replace(args):
    if len(args) == 0:
        filepath=input("Enter filepath of the replacement: ")
        sel_list=search([])
    elif len(args) == 1:
        filepath=args[0]
        sel_list=search([])
    else:
        filepath=args[0]
        sel_list=search(args[1:])
    if len (sel_list) != 1:
        print("Please select one entry for replacement!")
        return False
    item=sel_list[0]
    if os.path.exists(filepath) and os.path.isfile(filepath):
        title=item[2]
        if tb.replace_file(filepath, item):
            print(f"Replaced '{title}'!")
        else:
            print(f"Failed to replace '{title}'!")
    else:
        print("ERROR: Replacement doesn't exist or is not a file!")
        return False

def search(args,quiet=False):
    if len(args) == 0:
        print("Separate the items with spaces.")
        print("FILTERS: -a: All types\n\t -c: Category\n\t -f: Filename\n\t -g: Tags")
        print("\t -h: Hash\n\t -i: Content\n\t -s: Source\n\t -t: Title")
        args=input("Query: ")
        if len(args) > 0:
            res=tb.search_index(args.split(' '),quiet)
        else:
            print("\nQuery empty!")
            return [""]
    else:
        res=tb.search_index(args,quiet)
    return res

def show(args):
    tres=search(args,False)
    if not tres[0] == "":
        for res in tres:
            print("Title: ",res[2])
            print("\tSource:\t ",res[3])
            alias=ctb.get_alias(res[4])
            if alias != "":
                print("\tCategory: {} ({})".format(alias,res[4]))
            else:
                print("\tCategory:",res[4])
            print("\tFilename:",res[0],"(Encrypted)" if etb.is_encrypted(os.path.splitext(res[0])[0]) else "")
            print("\tHash:\t ",res[1])
            tags_list=[]
            for tag in res[5].split(","):
                alias=ttb.get_alias(tag)
                if alias != "":
                    tags_list.append(alias)
            print("\tTags:\t ",",".join(tags_list))
            print("\tContent: ",res[6])

def stats(args):
    verbose=False
    for arg in args:
        if re.match('[-]+[vV].*', arg):
            verbose=True
    entrynum=len(tb.get_col("HASH"))
    encnum=len(etb.get_col("NAME"))
    print(f"Entry count: {entrynum} ({encnum} encrypted)")
    tags=ttb.get_col("NAME")
    tagnum=len(tags)
    print(f"Tags count: {tagnum}")
    if verbose:
        for item in tags:
            name=item[0]
            taguse=tb.get_item("TAGS", name)
            if taguse[0] == "":
                taguse=0
            else:
                taguse=len(taguse)
            print(f"\t{ttb.get_alias(name)} ({name}): Used by {taguse} entr%s"% ("y" if taguse == 1 else "ies"))
    cats=ctb.get_col("NAME")
    catnum=len(cats)
    print(f"Categories count: {catnum}")
    if verbose:
        for item in cats:
            name=item[0]
            catuse=len(tb.get_item("CATEGORY", name))
            print(f"\t{ctb.get_alias(name)} ({name}):  Used by {catuse} entr%s"% "y" if taguse == 1 else "ies")

    return True

def main():
    if len(sys.argv) <= 1 or re.match('[hH].*',sys.argv[1]):
        help(sys.argv[2:])
    else:
        command=sys.argv[1]
        args=[]
        for arg in sys.argv[2:]:
            global bencrypt
            if re.match("[-]{1,2}e.*",arg): # -e --encrypted
                bencrypt=True
            if re.match("[-]{1,2}p.*",arg): # -p --plain
                bencrypt=False
            else:
                args.append(arg)
        if re.match('[aA].*',command):
            add(args)
        elif re.match('[mM].*',command):
            meta(args)        
        elif re.match('[cC][hH].*',command) or re.match('[cC].*[eE].*',command):
            check(args)
        elif re.match('[cC].*',command):
            copy(args)
        elif re.match('[dD].*',command):
            delete(args)
        elif re.match('[iI].*',command):
            imports(args)
        elif re.match('[oO].*',command):
            opens(args) 
        elif re.match('[rR].*',command):
            replace(args)
        elif re.match('[sS][tT].*',command) or re.match('[sS].*[aA].*',command):
            stats(args)
        elif re.match('[sS].*',command):
            show(args)
        elif re.match('[uU].*',command):
            update(args)
        else:
            print("No such option!")
    tb.connection.close()
    ctb.connection.close()
    etb.connection.close()
    ttb.connection.close()
    #input("Press return...")

if __name__ == "__main__":
    bencrypt=ENCRYPT # boolean for encryption in session
    filepath=INDEX_FILE
    etb = enctable(filepath)
    tb = filestable(filepath)
    ctb = metatable("CATEGORY",filepath)
    ttb = metatable("TAGS",filepath)
    main()