import os
import sys
import os.path
import platform
import sys
import shutil
import psutil
import signal
import inspect
import traceback
from pathlib import Path
import datetime
import glob
import re
import numbers
import csv
import unicodedata
import math
import numpy as np
import matplotlib.colors as mcolors

import tklib.tkimport as imp
mpl = imp.import_lib("matplotlib", stop_by_error = False)
if mpl is not None:
    from matplotlib.markers import TICKLEFT, TICKRIGHT, TICKUP, TICKDOWN, CARETLEFT, CARETRIGHT, CARETUP, CARETDOWN

from tklib.tkobject import tkObject, _analyze_varstr
import tklib.tkre as tkre
import tklib.collabo.RH.Ryu_colormap as ryu_cm


linestyles = ['solid', 'dashed', 'dashdot', 'dotted']

#colors = ['black', 'red', 'blue', 'darkgreen', 'darkorange', 'navy', 
#          'slategray', 'hotpink', 'olive', 'chocolate', 'magenta', 
#          'green', 'yellow', 'cyan']
colors = ryu_cm.colors
colors = ["black", "red", "blue", colors[0], colors[5], colors[8], colors[2], colors[9]]

markers = [
    ('.', 'point'),
    (',', 'pixel'),
    ('o', 'circle'),
    ('v', 'triangle_down'),
    ('^', 'triangle_up'),
    ('<', 'triangle_left'),
    ('>', 'triangle_right'),
    ('1', 'tri_down'),
    ('2', 'tri_up'),
    ('3', 'tri_left'),
    ('4', 'tri_right'),
    ('8', 'octagon'),
    ('s', 'square'),
    ('p', 'pentagon'),
    ('*', 'star'),
    ('h', 'hexagon1'),
    ('H', 'hexagon2'),
    ('+', 'plus'),
    ('x', 'x'),
    ('D', 'diamond'),
    ('d', 'thin_diamond'),
    ('|', 'vline'),
    ('_', 'hline'),
    ]
if mpl is not None:
    markers.append([
        (TICKLEFT, 'tickleft'),
        (TICKRIGHT, 'tickright'),
        (TICKUP, 'tickup'),
        (TICKDOWN, 'tickdown'),
        (CARETLEFT, 'caretleft'),
        (CARETRIGHT, 'caretright'),
        (CARETUP, 'caretup'),
        (CARETDOWN, 'caretdown'),
        ('None', 'nothing'),
        (None, 'nothing'),
        (' ', 'nothing'),
        ('', 'nothing')
        ])

markers = ['o', 's', '+', 'x', 'D', 'v', '^', '<', '>', '8', 'h', 'H']

nlinestyles = len(linestyles)
ncolors  = len(colors)
nmarkers = len(markers)

# ギリシャ文字マッピング辞書
GREEK_MAP = {
    "GAMMA": "Γ",
    "ALPHA": "Α",
    "BETA": "Β",
    "DELTA": "Δ",
    "EPSILON": "Ε",
    "ZETA": "Ζ",
    "ETA": "Η",
    "THETA": "Θ",
    "IOTA": "Ι",
    "KAPPA": "Κ",
    "LAMDA": "Λ",
    "MU": "Μ",
    "NU": "Ν",
    "XI": "Ξ",
    "OMICRON": "Ο",
    "PI": "Π",
    "RHO": "Ρ",
    "SIGMA": "Σ",
    "TAU": "Τ",
    "UPSILON": "Υ",
    "PHI": "Φ",
    "CHI": "Χ",
    "PSI": "Ψ",
    "OMEGA": "Ω",

    "gamma": "γ",
    "alpha": "α",
    "beta": "β",
    "delta": "δ",
    "epsilon": "ε",     
    "zeta": "ζ",
    "eta": "η",
    "theta": "θ",
    "iota": "ι",
    "kappa": "κ",
    "lamda": "λ",
    "mu": "μ",
    "nu": "ν",
    "xi": "ξ",
    "omicron": "ο",
    "pi": "π",  
    "rho": "ρ",
    "sigma": "σ",
    "tau": "τ",
    "upsilon": "υ",
    "phi": "φ",
    "chi": "χ",
    "psi": "ψ",
    "omega": "ω"       
}

def convert_color(color):
    if color is None:
        return None

    # matplotlib の CSS4_COLORS を辞書として利用
    # https://matplotlib.org/stable/gallery/color/named_colors.html
    color_dict = {key.lower(): value for key, value in mcolors.CSS4_COLORS.items()}

    # 入力された色名をチェック
    if color.lower() in color_dict.keys():
        return color_dict[color.lower()]

    # 色名でない場合は そのまま返す
    return color


def graph_style_idx(idx):
    icolor = idx % ncolors
    ils    = idx // ncolors % nlinestyles
    return colors[icolor], linestyles[ils]

def remove_comments(line):
    # 文字列内の # を保護するため、クォート部分を処理
    pattern = r'(["\'])(.*?)(\1)|(#.*)'
    parts = re.findall(pattern, line)
    result = ''
    
    for part in parts:
        if part[3]:  # コメント部分 (#以降)
            continue
        result += part[0] if part[0] else part[1]
    return result.strip()

def raise_error(desc = "custom error"):
    class CustomError(Exception):
        pass

    raise CustomError(desc)

def get_position(self = None, _class = None):
    frame = inspect.currentframe().f_back
    file_name = frame.f_globals["__file__"]
    function_name = frame.f_code.co_name
    if _class:
        return {"filename": file_name, "function": function_name, "line": line}
    elif self:
        class_name = self.__class__.__name__
        return {"filename": file_name, "class": class_name, "function": function_name, "line": line}
    else:
        return {"filename": file_name, "function": function_name, "line": line}

def get_position_str(self = None, _class = None):
    frame = inspect.currentframe().f_back
    file_name = frame.f_globals["__file__"]
    function_name = frame.f_code.co_name
    if _class:
        return f"In file: {file_name}, function: {function_name}, line: {line_number}"
    elif self:
        class_name = self.__class__.__name__
        return f"In file: {file_name}, class: {class_name}, function: {function_name}, line: {line_number}"
    else:
        return f"In file: {file_name}, function: {function_name}, line: {line_number}"

def print_position(self = None, _class = None, exc_type = None, exc_value = None, exc_tb = None):
    frame = inspect.currentframe().f_back
    file_name = frame.f_globals["__file__"]
    function_name = frame.f_code.co_name
    line_number = frame.f_lineno
    if _class:
        print(f"In file: {file_name}, function: {function_name}, line: {line_number}")
    elif self:
        class_name = self.__class__.__name__
        print(f"In file: {file_name}, class: {class_name}, function: {function_name}, line: {line_number}")
    else:
        print(f"In file: {file_name}, function: {function_name}, line: {line_number}")

    if exc_tb:
#        tb = traceback.extract_tb(exc_tb)
#        print("Traceback:")
#        for frame in tb:
#            print(f"  file: {frame.filename}, function: {frame.name}, line: {frame.lineno}")

        detailed_tb = traceback.format_exception(exc_type, exc_value, exc_tb)
        print("Error information:")
        for line in detailed_tb:
            print(line, end = '')

def get_class(instance):
    return instance.__class__

def get_definition_position(obj):
    if not inspect.isclass(obj): obj = obj.__class__

    file_name = inspect.getfile(obj)
    iline     = inspect.getsourcelines(obj)[1]
    return file_name, iline 

def print_process(proc_name = '', username = '', print_level = 1):
    nfound = 0
    proc_dict = {}
    for proc in psutil.process_iter(['pid', 'name', 'username', 'cmdline']):
        if username == 'all' or (proc.info['username'] and username in proc.info['username']):
            if proc_name == '' or proc_name in proc.name():
                nfound += 1

                args = ''
                if proc.info.get('cmdline', None):
                    filename = os.path.basename(proc.info['cmdline'][0])
                    args = ' '.join(proc.info['cmdline'][1:])
                    if len(args) >= 60: args = args[:60] + ' ...'
#            print(f"exec path: {filename}")
#            print(f"Command Line: ", proc.info['cmdline'][1:])

                if print_level:
                    print("-" * 40)
                    print(f"process name: {proc.info['name']}")
                    print(f"  args : {args}")
                    print(f"  owner: {proc.info['username']}")
                    print(f"  PID  : {proc.info['pid']}")

                proc_dict[proc.pid] = proc.info

    if print_level and nfound == 0:
        print(f"\nNo process found with {proc_name}.")

    return proc_dict

def kill_process_interactive(proc_name = '', username = '', pid = '', print_level = 1):    
    while True:
        if print_level: print()
        inf = print_process(proc_name, username, print_level = print_level)
        if print_level: 
            print()
            pid = input("input PID to kill [ENTER to terminate] >> ")

        if pid == '': break

        pid = int(pid)
        try:
#            os.kill(int(pid), signal.SIGTERM)
            os.kill(pid, signal.SIGKILL)
            if print_level: 
                print(f"Process {inf[pid]['name']} (PID: {pid}) terminated")
        except Exception as e:
            if print_level:
                print(f"Failed to kill process (PID: {pid}) due to error: {e}")
        

    if print_level: print()

def safe_getelement(var, key, defval = None):
    try:
        return var[key]
    except:
        return defval

def handle_exception(exc_type, exc_value, exc_tb):
    print(f"\nError:", end = '')
    print_position(exc_type = exc_type, exc_value = exc_value, exc_tb = exc_tb)
    input("Press ENTER to terminate>>\n")

def set_exception(func = handle_exception, print_level = 1):
    if print_level:
        print("Information in tkutils.set_exception(): Set excption handler sys.excepthook")
    sys.excepthook = handle_exception

def is_numeric(var):
    if var is None or type(var) is str: return False
    if math.isinf(var) or math.isnan(var): return False

    return isinstance(var, (int, float, np.float64, np.float32, np.int16, np.int8, numbers.Number))

def conv_float(val,  format = "{:.10g}"):
    if format is None or format == '':
        return val

    if type(val) is float:
        return format.format(val).strip()

    return val

def pconv_by_type_one(s, type = 'str', defval = None, strict = False):
    if strict:
        if type is int or type == 'int':
            try:
                return int(s)
            except:
                return defval
        if type is float or type == 'float' or type == 'double':
            try:
                return float(s)
            except:
                return defval
        if type is str or type == 'str':
            return f"{s}"
        return s
    else:
        if type is int or type == 'int':
            return pint(s, defval)
        if type is float or type == 'float':
            return pfloat(s, defval)
        if type is str or type == 'str':
            return f"{s}"
        return s

def pconv_by_type(s, type = 'str', defval = None, strict = False):
    if isinstance(type, str):
        types = type.split('|')
    else:
        types = [type]
    for t in types:
#        print("s=", s, types, t)
        ret = pconv_by_type_one(s, type = t, defval = None, strict = strict)
        if ret is not None:
            return ret

    return defval

def pconv(s, defval = 0, strict = True):
    if s is None:
        return defval

    try:
       return int(s)
    except:
        pass

    try:
       return float(s)
    except:
        return s


#    if re.match(r'^[+\-]?[\d]+$', s):
#        return pint(s, strict, defval)
#    if re.match(r'^[+\-]?[+-\d\.eEdD]+$', s):
##    if re.match(r'^[+\-]?[+\-\d\.eEdD]+$', s):
#        if re.match(r'd$', s):
#            return s
#        return pfloat(s, strict, defval)

    return s

def pint(s, defval = 0, strict = True):
    if s is None:
        return defval

    try:
        return int(s)
    except:
        pass

    if strict:
        return defval

    list = tkre.Search(r'([\+\-]?[\d]+)', s)
    try:
        return int(list[1])
    except:
        return defval

def pfloat(s, defval = 0.0, strict = True):
    if s is None:
        return defval

    try:
        return float(s)
    except:
        pass

    if strict:
        return defval

    list = tkre.Search(r'([\+\-]?[\d\.eE]+)', s)
    try:
        return float(list[1])
    except:
        if defval is None:
            return s
        return defval

def pintfloat(s, defval = 0.0, strict = True):
    try:
        return int(s)
    except:
        pass

    return float(s, defval = defval, strict = strict)

def str2val(s):
    if s is None   : return None
    if s == 'None' : return None
    if s == 'True' : return True
    if s == 'False': return False
    return pconv(s)

def val2str(val):
    if val is None:
        val = 'None'
    elif val is True:
        val = 'True'
    elif val is False:
        val = 'False'
    return val

def index2val(index, list_var, defval = (None, None)):
    itarget = None
    if type(index) is int:
        if len(list_var) <= index:
            return defval[0], defval[1]

        itarget = index
        index = list_var[itarget]
    else:
        v = pint(index, None)
        if v is not None:
            itarget = v
            index = list_var[itarget]
        else:
            for i, val in enumerate(list_var):
                if val == index:
                    itarget = i
                    break
            else:
                return None, None
                
    return itarget, index

def format_strlist(vars, format, separator = ', '):
    s = ""
    for i in range(len(vars)):
        if vars[i] is None:
            s += "None" + separator
        else:
            s += format.format(vars[i]) + separator
    return s.rstrip(separator)

def print_line(data_list, format = '{:^10}'):
        for d in data_list:
            print(format.format(d), end = '')
        print()

def print_data(labels, data_list, label_format = '{:^10}', data_format = '{:>10}', header = None, nmax = None, print_level = 0):
        if type(labels[0]) is list and type(labels[0]) is tuple:
            _l = []
            _d = []
            for l, d in zip(labels, data_list):
                _l.extend(l)
                _d.extend(d)
        else:
            _l = labels
            _d = data_list
        
        if header is not None:
            print("")
            print(header)

        ncol  = len(_d)
        ndata = len(_d[0])
        if nmax is None:
            nskip = 1
        else:
            nskip = max([1, int(ndata / nmax)])
        print_line(labels, label_format)
        for i in range(0, ndata, nskip):
            print_line([_d[icol][i] for icol in range(ncol)], data_format)


def pdb():
    print("Enter to pdb: see https://docs.python.org/ja/3/library/pdb.html")
    print("   h: help")
    print("   p, pp: print var")
    print("   what is: print type of var")
    import pdb; pdb.set_trace()


def is_list(var):
    t = type(var)
    if t is list or t is tuple:
        return True

    return False

def to_list(variable):
    if isinstance(variable, np.ndarray):
        return variable.tolist()
    elif isinstance(variable, pd.DataFrame):
        return variable.values.tolist()
    elif isinstance(variable, pd.Series):  # Seriesの場合も対応
        return variable.tolist()
    elif type(variable) is list:
        return variable
    else:
        return list(variable)

def minmax_xy(x = None, y = None, x0 = None, xstep = None, xmin = None, xmax = None):
    if x0 is None:
        _min =  1.0e300
        _max = -1.0e300
        for _x, _y in zip(x, y):
            if (xmin is not None and _x < xmin) or (xmax is not None and xmax < _x):
                continue
            
            if _min > _y:
                _min = _y
            if _max < _y:
                _max = _y

        return _min, _max
    else:
        n = len(y)
        idx0 = max([0, int((xmin - x0) / xstep + 1.0e-5)])
        idx1 = min([n-1, int((xmax - x0) / xstep + 1.0e-5)])
        _y = y[idx0:idx1 + 1]
        return min(_y), max(_y)

def get_max_index(list, axis = 0):
    return np.arg_max(list, axis = axis)
#    return list.index(max(list))

def get_min_index(list, axis):
    return np.arg_min(list, axis = axis)
#    return list.index(in(list))
    
def find_range_indexes(x, xmin, xmax):
    n = len(x)
    
    ix0 = 0
    for idx in range(n):
        if x[idx] < xmin:
            ix0 = idx
        else:
            break

    ix1 = n
    for idx in range(n-1, -1, -1):
        if x[idx] > xmax:
            ix1 = idx
        else:
            break

    return ix0, ix1

def transpose_list2d(data_list):
    return list(map(list, zip(*data_list)))

def sort_lists(lists):
    zip_lists = zip(*lists)
    zip_sort = sorted(zip_lists)
    return list(zip(*zip_sort))

pattern_int   = re.compile(r'^[-+]?[0-9]+$')
pattern_float = re.compile(r'^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$')
def is_float(s):
    t = type(s)
    if t is float:
        return True
    
    if t is str:
        if pattern_float.match(s):
            return True

    return False

def is_int(s):
    t = type(s)
    if t is int:
        return True
    
    if t is str:
        if pattern_int.match(s):
            return True

    return False

def analyze_varstr(str):
    return _analyze_varstr(str)

def split_optstr(str):
    try:
        val, id = str.split(':')
        return pfloat(val), pint(id)
    except:
        return str, None

def quote_command_if_space(s, special_chars = "", quote_blank = True):
    if s is None:
        return s
    
    if s == '':
        return '""'

    if ' ' not in s and not re.search(rf"[{special_chars}]", s):
        pass
    elif len(s) >= 1 and '/-' in s:
        pass
    elif re.match(r"\s*[\"\'].*[\"\']", s):
        pass
    else:
        s = f'"{s}"'
    return s

def convert_by_vars(s, vars, prefix = r'\$', bra = r'\(', ket = r'\)'):
    idebug = 0

    if idebug >= 1:
        print("")
        print("In tkutils.convert_by_vars:")
        print(f"  vars={vars}")

    ret = ''
    rest = s
    if idebug >= 2:
        print(f"89 rest={rest}")
        print(f"90 ret={ret}")
    while 1:
        if idebug >= 2:
            print(f"93 rest={rest}")
            print(f"94 ret={ret}")

        if prefix != '':
# Separate by '$'
            m = re.match(rf'(.*?){prefix}(.*)$', rest, flags = re.MULTILINE | re.DOTALL)
#            m = re.match(r'(.*?)\$(.*)$', rest, flags = re.MULTILINE | re.DOTALL)
            if not m:
                if idebug >= 1:
                    print(f" not matched for $: rest={rest}")
                return ret + rest

            if idebug >= 1:
                print(f" match for $: rest={rest}")
                print(f"              ret={ret}")

            g = m.groups()
            pre = g[0]
            n   = len(pre)
            rest = g[1]
            if idebug >= 2:
                print(f"  pre={pre} rest={rest}")

# Case rest = 'abc\$def', should be converted to 'abc$def'
            if n > 0 and pre[n-1] == '\\':
                ret += pre[0:n-1] + '$'
            else:
                ret += pre

# Case rest = 'abc$i def', should be converted to 'abc[ini_path] def'
            m = re.match(r'([a-zA-Z])([^\w_].*)?$', rest, flags = re.MULTILINE | re.DOTALL)
            if m:
                if debug >= 1:
                    print(f"121: not matched")
                g = m.groups()
                n = len(g)
                varname = g[0].lower()
                rest = g[1]
                if rest is None:
                    rest = ''

                val = vars.get(varname, None)
#                    print(f"   1{var=} {val=}")
                if val is None:
                    app.show_error_dialog(f"Invalid varname [${varname}]")
                    val = f"??{varname}??"

                ret += val
                if idebug >= 2:
                    print(f"   1 ret={ret}")

                continue
        else:
            m = re.match(rf'(.*?)({bra}.*)$', rest, flags = re.MULTILINE | re.DOTALL)
            if not m:
                if idebug >= 1:
                    print(f" not matched for $: rest={rest}")
                return ret + rest

            g = m.groups()
            ret += g[0]
            rest = g[1]

# Case rest = 'abc$(tkProgRoot)def', should be converted to 'abc[tkProgRoot]def'
        m = re.match(rf'{bra}(\w+){ket}(.*)$', rest, flags = re.MULTILINE | re.DOTALL)
#        m = re.match(r'\((\w+)\)(.*)$', rest, flags = re.MULTILINE | re.DOTALL)
        if m:
            if idebug >= 1:
                print(f"146: not matched rest={rest}")
            g = m.groups()
            varname = g[0].lower()
            rest = g[1]

#            print(f"{varname=}")
            val = vars.get(varname, None)
            if idebug >= 2:
                 print(f"   3 varname={varname} val={val}")
                 print(f"   3 vars={vars} val={val}")
            if val is None:
                print("")
                print(f"Warning in tkutils.convert_by_vars(): Invalid varname [{prefix}{bra}{varname}{ket}]")
                val = f"??{varname}??"

            ret += f"{val}"
            if idebug >= 2:
                print(f"   4 ret={ret}")

            continue

#      print(f"193 ret={ret}")
    return ret


def del_quote(s):
    s = s.strip()
    n = len(s)
    if n <= 1:
        return s

    if s[0] == '"' and s[n-1] == '"':
        return s[1:n-1]
    if s[0] == "'" and s[n-1] == "'":
        return s[1:n-1]
    return s

def add_quotation(str, quotation, quotation_original):
    if quotation == 'remove':
        s = del_quote(str)
        return s
    elif quotation == 'keep':
        s = quotation_original + str + quotation_original
        return s

    return quotation + str + quotation

def split_quoted_args(s, sep = r'\s+', quotation = 'remove'):
#    arg_list = s.split()

    rest = s.strip()
    if rest == '':
        return rest

    args = []
    while 1:
        if rest[0] == '"':
            m = re.match(rf'(.*?)\"{sep}(.*)$', rest, flags = re.MULTILINE | re.DOTALL)
            if not m:
                args.append(del_quote(rest))
                break

            a1 = m.groups()[0][1:]
            rest = m.groups()[1]
            args.append(add_quotation(a1, quotation, '"'))
        elif rest[0] == "'":
            m = re.match(rf'(.*?)\'{sep}(.*)$', rest, flags = re.MULTILINE | re.DOTALL)
            if not m:
                args.append(del_quote(rest))
                break

            a1 = m.groups()[0][1:]
            rest = m.groups()[1]
            args.append(add_quotation(a1, quotation, "'"))
        else:
            m = re.match(rf'(.*?){sep}(.*)$', rest, flags = re.MULTILINE | re.DOTALL)
            if not m:
                args.append(del_quote(rest))
                break

            a1 = m.groups()[0]
            rest = m.groups()[1]
            args.append(a1)

    return args

def split_two(s, sep = r'\s+', quotation = 'remove', defval = ''):
    s = s.strip()

    if len(s) == 0:
        return '', ''

    if s[0] == '"':
        m = re.match(rf'(.*?)\"{sep}(.*)$', s, flags = re.MULTILINE | re.DOTALL)
        if m:
            g = m.groups()
            s1 = g[0][1:]
            s2 = g[1]
#            print(f"260 s1={s1} s2={s2}")
            return add_quotation(s1, quotation, '"'), add_quotation(s2, quotation, '"')
    elif s[0] == "'":
        m = re.match(rf'(.*?)\'{sep}(.*)$', s, flags = re.MULTILINE | re.DOTALL)
        if m:
            g = m.groups()
            s1 = g[0][1:]
            s2 = g[1]
#            print(f"268 s1={s1} s2={s2}")
            return add_quotation(s1, quotation, "'"), add_quotation(s2, quotation, "'")

    m = re.match(rf'(\S+){sep}(.*)\s*$', s, flags = re.MULTILINE | re.DOTALL)
    if not m:
#        print(f"273 s1={s}")
        return s, defval

    g = m.groups()
#    print(f"276 s1={g[0]} s2={g[1]}")

    return g[0], g[1]

def split_command_line(s, quotation = 'remove'):
    cmd, rest = split_two(s, quotation = quotation)
    args = split_quoted_args(rest, quotation = 'remove')
    
    return cmd, rest, args

def mprint(*args, fp = None, **kwargs):
    print(*args, **kwargs)
    if fp:
        print(*args, file = fp, **kwargs)
        fp.flush()

def get_path_list(varname = 'PATH'):
    return getenv(varname, '').split(os.pathsep)

def desktop_name():
    os_name = platform.system()
    if os_name == 'Windows':
        return 'Windows'
    elif os_name == 'Darwin':
        return 'Darwin'

    ret = getenv('XDG_CURRENT_DESKTOP', '')
    if ret != '':
        return ret
    return None

    
def add_path(path, varname = 'PATH'):
    os_name = get_os()
    sep = os.pathsep
    paths = get_path_list(varname = varname)

    path_list = [path]
    is_found = False
    if os_name == 'Linux':
        for p in paths:
            if p == path:
                return os.environ.get(varname, "")
            path_list.append(p)
    else:
        path = path.lower()
        for p in paths:
            if p.lower() == path:
                return os.environ.get(varname, "")
            path_list.append(p)

#    print("path=", path)
#    print("path_list=", path_list)
    os.environ[varname] = sep.join(path_list)
    
    return os.environ.get(varname, "")

def normalize_str(str, conv_table = None, target_char = ' ', unicodedata_mode = 'NFKC', ):
    if str is None:
        return str

    if conv_table is None:
        conv_table = str.maketrans({
              '\u3000': target_char
            , ' '     : target_char
            , '\t'    : target_char
            , '\r'    : target_char
            , ', '    : target_char
            , ','     : target_char
            , '，'    : target_char
            })

    str = str.translate(conv_table)

    str = unicodedata.normalize(unicodedata_mode, str)

    return str

def normalize_name(name, conv_table, unicodedata_mode = 'NFKC', upper_mode = 'upper'):
    name = normalize_str(name, conv_table = conv_table, unicodedata_mode = unicodedata_mode)
    name = name.replace('  ', ' ')
    name = name.strip()
    if upper_mode == 'upper':
        name = name.upper()
    elif upper_mode == 'lower':
        name = name.lower()
    elif upper_mode == 'capitalize':
        name = name.capitalize()
    elif upper_mode == 'title':
        name = name.title()

    return name

def add_dict(var, key1, key2 = None):
    if key2 is None:
        if var.get(key1, None) is None:
            var[key1] = 1
        else:
            var[key1] += 1
    else:
        if var.get(key1, None) is None:
            var[key1] = {}
        if var[key1].get(key2, None) is None:
            var[key1][key2] = 1
        else:
            var[key1][key2] += 1

def get_os_info(terse = False):
    return platform.system(), platform.platform(terse = terse),\
              platform.release(), platform.version()

# macOS: 'Darwin', Windows: 'Windows', Linux: 'Linux'
def get_os():
    return platform.system()

def is_windows():
    if platform.system() == 'Windows':
        return platform.version()
    return False

def is_mac():
    if platform.systmem() == 'Darwin':
        return platform.version()
    return False

def is_linux():
    if platform.systmem() == 'Linux':
        return platform.version()
    return False

def getenv(key, defval = None):
    return os.environ.get(key, defval)

def setenv(key, val = None):
    if val is None:
        if key in os.environ[key]:
            del os.environ[key]
            return
    os.environ[key] = val

def GetPathDelimiter():
    return os.sep

def get_path_separator():
    return os.sep

def get_PATH_env_separator():
    return os.pathsep

def get_ext_separator():
    return os.extsep

def is_dir(path):
    return os.path.isdir(path)

def IsDir(path):
    return os.path.isdir(path)

def is_file(path):
    return os.path.isfile(path)

def IsFile(path):
    return os.path.isfile(path)

def is_exist(path):
    return os.path.exists(path)

def Exists(path):
    return os.path.exists(path)

def make_path(dir, *args):
    return os.path.join(dir, *args)

def MakePath(dir, *args):
    return make_path(dir, *args)

def get_fullpath(path):
    return os.path.abspath(path)

def get_ext(path):
    dirname, basename, filebody, ext = split_file_path(path)
    return ext

def get_dirname(path):
    fullpath      = os.path.abspath(path)
    dirname       = os.path.dirname(fullpath)
    return dirname
    
def get_last_directory(path, check_dir = False):
    if check_dir and os.path.isdir(path):
        dirname, last_dir, filebody, ext = split_file_path(path)
        return last_dir
    else:
        dirname, basename, filebody, ext = split_file_path(path)
        last_dir = os.path.basename(os.path.normpath(dirname))
        return last_dir

def get_upper_directory(path):
    dirname, basename, filebody, ext = split_file_path(path)
    return dirname
    
def split_drive(path):
    dirname, basename, filebody, ext = split_file_path(path)
    last_dir = os.path.basename(os.path.normpath(dirname))
    drive = ''
    dirname_wo_drive = dirname
    m = re.match(r'([a-zA-Z]:)(.*)$', dirname)
    if m:
        drive = m.groups()[0]
        dirname_wo_drive = m.groups()[0]
    return drive, dirname_wo_drive

def split_file_path(path, check_dir = False):
    if os.sep == '\\':
        path0 = tkre.Sub(r'\\', '/', path)
    else:
        path0 = path
    path0 = tkre.Sub('/$', '', path0)

    if check_dir and os.path.isdir(path):
        return path, '', '', ''

#    dirname, basename, os.path.split(path)
    basename    = os.path.basename(path0)
    dirname     = os.path.dirname(path0)
    header, ext = os.path.splitext(path0)
    filebody    = os.path.basename(header)

    if os.path.isdir(path):
        ext = ''
    if os.sep == '\\':
        dirname = tkre.Sub('/', r'\\', dirname)

    return dirname, basename, filebody, ext

def SplitFilePath(path):
    return split_file_path(path)

def modify_path(path, addpath):
    dirname, basename, filebody, ext = split_file_path(path)
    return make_path(dirname, filebody + addpath)

def replace_path(path, template, ext_dict = {}):
    fullpath      = os.path.abspath(path)
    filename      = os.path.basename(fullpath)
    dirname       = os.path.dirname(fullpath)
    filebody, ext = os.path.splitext(filename)
    ext_dict.update({
        "fullpath": fullpath,
        "dirname": dirname,
        "filename": filename,
        "filebody": filebody,
        "ext": ext
        })

    return template.format(**ext_dict)

def get_user_inifile_dir(env_vars = ['HOME'], dir_names = ['.']):
    HOME = None
    for env in env_vars:
        HOME = os.environ.get(env, None)
        if HOME is None:
            continue

    if HOME is None:
        return None

    for dirname in dir_names:
        user_ini_dir = os.path.join(HOME, dirname)
        if os.path.exists(user_ini_dir):
            return user_ini_dir

        try:
            os.mkdir(user_ini_dir)
        except:
            continue

    return None

def get_file_list(path, filemask = '*.*', filename_only = True):
    if not os.path.isdir(path):
        path = os.path.dirname(path)

    files_dict = {}
    dirs_dict  = {}
    for fm in filemask.split(';'):
        p = os.path.join(path, fm)
        for f in glob.glob(p):
            isdir = os.path.isdir(f)

            if filename_only:
                f = os.path.basename(f)

            if isdir:
                dirs_dict[f"[{f}]"] = 1
            else:
                files_dict[f] = 1

    dirs = ['[..]']
    dirs.extend(list(dirs_dict.keys()))

    return dirs, sorted(list(files_dict.keys()))

def get_file_masks(key = 'executables'):
    if is_windows():
        if key == 'executables':
            exts = getenv('PATHEXT', '').lower().split(';')
            return ["*{}".format(ext) for ext in exts] 
        return ["*.exe", "*.com", "*.bat", "*.vbs", "*.sh", "*.csh"]

    return ["*.*"]

def find_files(filename, start_dir = None):
    if start_dir is None:
        start_dir = os.get.cwd()

    path_list = []
    for cur_dir, dirs, files in os.walk(start_dir):
        for i in range(len(files)):
#            print("path=", cur_dir, files[i])
            if filename == files[i]:
#                print("  hit:", cur_dir, files[i])
                path_list.append(os.path.join(cur_dir, files[i]))
#            yield os.path.join(cur_dir, file)

    return path_list

def find_latest_file(filename, start_dir = None, defval = None):
    path_list = find_files(filename, start_dir = start_dir)
    if len(path_list) == 0:
        return defval

    mtime = 0
    ret_path = None
    for path in path_list:
        mtime1 = os.path.getmtime(path)
#        print("path=", path, mtime1)
        if mtime1 >= mtime:
            ret_path = path
    
    return ret_path

def find_executable_path(filename, defval = None, filename_only = False):
    paths = get_path_list()
    for path in paths:
#        print("path:", path)
        f = f"{path}{os.sep}{filename}"
#        print("  f:", f)
        if Exists(f) and os.path.getsize(f) > 0:
#            print("    Exists")
            if filename_only:
                return filename
            else:
                return f

        if is_windows():
            exts = getenv('PATHEXT', '').lower().split(';')
            for ext in exts:
                p = f'{f}{ext}'
#                print("  p:", p)
                if Exists(p) and os.path.getsize(p) > 0:
#                    print("    Exists")
                    if filename_only:
                        return filename
                    else:
                        return p
    return defval

def search_executable_path(filenames, defval = None, filename_only = False):
    for f in filenames:
        path = find_executable_path(f, defval = None, filename_only = filename_only)
        if path:
            return path
    return defval

def terminate(message = None, usage = None, pause = False):
    if message is not None:
        print("")
        print(message)

    if usage:
        print("")
        usage()

    print("")
    print("")
    if pause:
        input("Press ENTER to terminate >> ")
        print("")

    exit()

def sfmt(str, fmt):
    f = '{' + fmt + '}'
    return f.format(str).strip()

def getarg(position, defval = None):
    try:
        return sys.argv[position]
    except:
        return defval

def getfloatarg(position, defval = None):
    s = getarg(position, defval)
    v = pfloat(s, defval)
    return v

def getintarg(position, defval = None):
    s = getarg(position, defval)
    v = pint(s, defval)
    return v
    
def GetList(list, n, defval = 0.0):
    l = list.copy()
    for i in range(len(l), n):
        l.append(defval)
    return l[0:n]

def validate_error(y0, y1, eps, message):
    if y0 == 0.0:
        err = (y1 - y0) / y1
    else:
        err = (y1 - y0) / y0
    if abs(err) > eps:
        print("")
        print("{}Too large error {} vs {} error={:8.3g} > {:8.3g}".format(message, y0, y1, err, eps))
        print("")
        exit()

def read_csv_to_dict(path, delimiter = ',', print_level = 1):
    try:
        f = open(path)
    except Exception as e:
        if print_level:
            print(f"Warning in tkutils::read_csv_to_dict(): Can not read [{path}]")
            return None, None

    _dict = {}
    reader = csv.reader(f)
    labels = next(reader)
    for l in labels:
        _dict[l] = []

    for row in reader:
        for i, l in enumerate(labels):
            _dict[l].append(pconv(row[i]))

    return labels, _dict


def read_csv_to_dict_list(path, delimiter = ',', newline = '', convert_float = 0, convert_int = 0, print_level = 1):
    try:
        f = open(path)
    except Exception as e:
        if print_level:
            print(f"Warning in tkutils::read_csv_to_dict_list(): Can not read [{path}]")
            return None

    inf = []
    reader = csv.DictReader(f, delimiter = delimiter)#, newline = newline)
    for row in reader:
        inf.append(row)
    if convert_float:
        for i in range(len(inf)):
            for key, val in inf[i].items():
                 inf[i][key] = pfloat(inf[i][key], None)
    elif convert_int:
        for i in range(len(inf)):
            for key, val in inf[i].items():
                 inf[i][key] = pint(inf[i][key], None)
    return inf

def read_csv(path, delimiter = ',', newline = '',
            first_header = 1, convert_float = 0, convert_int = 0):
    inf = []
    header = None
    try:
        with open(path) as f:
            reader = csv.reader(f, delimiter = delimiter, newline = newline)
            for row in reader:
                inf.append(row)
        if first_header:
            header = inf[0]
            inf    = inf[1:]
        if convert_float:
            for i in range(len(inf)):
                for j in range(len(inf[i])):
                     inf[i][j] = pfloat(inf[i][j], None)
        elif convert_int:
            for i in range(len(inf)):
                for j in range(len(inf[i])):
                     inf[i][j] = pint(inf[i][j], None)
        return header, inf
    except:
        return None, None

def read_csv2(fname, delimiter = ','):
    print("")
    with open(fname) as f:
        fin = csv.reader(f, delimiter = delimiter)
        
        labels = next(fin)
        xlabel = labels[0]

# label行が 空文字 の場合、データとしては読み込まない
        ylabels = []
        for i in range(1, len(labels)):
            if labels[i] == '':
                break
            ylabels.append(labels[i])
        ny = len(ylabels)
        print("xlabel: ", xlabel)
        print("ylabels: ", ylabels)
        print("ny=", ny)

        x     = []
        ylist = []
        for i in range(ny):
            ylist.append([])
        
        for row in fin:
            x.append(pfloat(row[0]))
            for i in range(1, ny+1):
                v = pfloat(row[i])
                if v is not None:
                    ylist[i-1].append(v)
                else:
                    ylist[i-1].append(None)

    return [xlabel, *ylabels], [x, *ylist]

    
def save_csv(path, headerlist, datalist, is_print = 0):
    f = open(path, 'w')
    if not f:
        return 0

#    print("len=", len(datalist), len(datalist[0]))
    writer = csv.writer(f, delimiter=',', lineterminator='\n')
    writer.writerow(headerlist)
    for i in range(len(datalist[0])):
        dlist = [datalist[id][i] for id in range(len(datalist))]
        if is_print:
            try:
                dliststr = joinf(dlist, "%12.8g", ", ")
            except:
                dliststr = joinf(dlist, "%12s", ", ")
            print("  {:3d}: {}".format(i, dliststr))

        writer.writerow(dlist)

    f.close()
    return 1

def joinf(list, format, sep):
    s = format % (list[0])
    for i in range(1, len(list)):
        s += sep + format % (list[i])
    return s

def merge_attributes(tobj, sobj):
    if sobj is None:
        return tobj
    for key, val in sobj.__dict__.items():
        try:
            getattr(tobj, key)
        except:
            setattr(tobj, key, val)
    return tobj
    

def check_attributes(obj, isprint = 0, *args):
    list = []
    for key in args:
        try:
            getattr(obj, key)
        except:
            list.append(key)
    if len(list) == 0:
        return None
    else:
        if isprint:
            print("tkutils.check_attributes: The following attributes are not defined:",
                list)
        return list


def lvlprint(lprint, level, *args):
    if lprint >= level:
        print(*args)

def BuildCreationDateStr(date = None):
    if date == None:
        date = datetime.datetime.now()
    s = "%04d/%02d/%02d" % (date.year, date.month, date.day)
    return s

def copy_path(path, newpath):
    try:
        shutil.copy2(path, newpath, follow_symlinks = True)
        return 1
    except:
        return 0

def rename_file(path, newpath):
    try:
        os.rename(path, newpath)
        return 1
    except:
        return 0

def delete_file(path):
    try:
        os.remove(path)
        return 1
    except:
        return 0


