module otya.smilebasic.project;
import otya.smilebasic.error;
import otya.smilebasic.data;
import otya.smilebasic.type;
import std.path;
import std.file;
import std.string;
import std.traits;
import std.conv;
import std.uni;
import std.typecons;
import std.range;
enum DialogResult
{
    FAILURE = 0,
    SUCCESS = 1,
    CANCEL = -1,
}

class Projects
{
    wstring currentProject;
    wstring rootPath;
    wstring projectPath;
    string projectPathU8;
    DialogResult result = DialogResult.FAILURE;//Default value
    wstring saveConfirm = 
r"ファイルを書き込みます。

          名前: %s

よろしいですか？
";
    this(wstring root)
    {
        rootPath = root;
        projectPath = buildPath(root, "PROJECTS"w);
        projectPathU8 = projectPath.to!string;
        if(exists(projectPathU8))
        {
            if(isFile(projectPathU8))
            {
                throw new Exception("PROJECTS is file");
            }
        }
        else
        {
            mkdir(projectPathU8);
        }
        createProjectInternal("[DEFAULT]");
        createProjectInternal("SYS");
    }
    static bool isValidProjectName(C)(C[] filename) if(isSomeChar!(C))
    {
        if(filename.length > 14)
        {
            return false;
        }
        foreach(c; filename)
        {
            if(!((c >= 'A' && c<= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-'))
            {
                return false;
            }
        }
        return true;
    }
    static bool isValidFileName(C)(C[] filename) if(isSomeChar!(C))
    {
        if(filename.length > 12)
        {
            return false;
        }
        foreach(c; filename)
        {
            if(!((c >= 'A' && c<= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-' || c == '@'))
            {
                return false;
            }
        }
        return true;
    }
    private void createProjectInternal(wstring name)
    {
        auto path = buildPath(projectPath, name.toUpper).to!string;
        if(!exists(path))
        {
            mkdir(path);
        }
        auto txt = buildPath(path, "TXT").to!string;
        if(!exists(txt))
        {
            mkdir(txt);
        }
        auto dat = buildPath(path, "DAT").to!string;
        if(!exists(dat))
        {
            mkdir(dat);
        }
    }
    static Tuple!(wstring, wstring, wstring) splitResourceName(wstring name)
    {
        auto ind = name.indexOf(":");
        wstring type, project;
        if(ind != -1)
        {
            type = name[0..ind];
            name = name[ind + 1..$];
        }
        ptrdiff_t sep = name.indexOf("/");
        if(sep != -1)
        {
            project = name[0..sep];
            name = name[sep + 1..$];
        }
        return tuple(type, project, name);
    }
    wstring[] getFileList(wstring project, wstring type)
    {
        import std.algorithm;
        import std.functional;
        if (project.empty)
            project = convertProjectName(currentProject);
        else if(!isValidProjectName(project))
        {
            return null;
        }
        auto dir(wstring type)
        {
            auto path = buildPath(projectPath, project, type).to!string;
            return dirEntries(path, SpanMode.shallow, false);
        }
        
        if(type == "")
        {
            return chain(dir("TXT").filter!(x=>x.isFile&&isValidFileName(baseName(x.name))).map!((x) => "*"w ~ baseName(x.name).to!wstring),
                         dir("DAT").filter!(x=>x.isFile&&isValidFileName(baseName(x.name))).map!((x) => " " ~ baseName(x.name).to!wstring)).array;
        }
        if(type == "TXT")
        {
            return dir("TXT").filter!(x=>x.isFile&&isValidFileName(baseName(x.name))).map!((x) => "*" ~ baseName(x.name).to!wstring).array;
        }
        if(type == "DAT")
        {
            return dir("DAT").filter!(x=>x.isFile&&isValidFileName(baseName(x.name))).map!((x) => " " ~ baseName(x.name).to!wstring).array;
        }
        return null;
    }
    bool loadFile(wstring project, wstring type, wstring name, out wstring contents)
    {
        contents = "";
        result = DialogResult.FAILURE;
        if(!isValidProjectName(project))
        {
            return false;
        }
        type = type.toUpper;
        //TODO:case-sensitive filesystem...
        name = name.toUpper;
        if (project.empty)
            project = convertProjectName(currentProject);
        if(type != "TXT" && type != "DAT")
        {
            return false;
        }
        if(!isValidFileName(name))
        {
            return false;
        }
        auto path = buildPath(projectPath, project, type, name).to!string;
        if (!path.exists)
        {
            //show dialog
            return false;
        }

        //show dialog
        result = DialogResult.SUCCESS;
        contents = readText(path).to!wstring;
        return true;
    }

    bool loadDataFile(T)(FileName file, ref Array!T contents)
        if (is(T == int) || is(T == double))
    {
        wstring project;
        result = DialogResult.FAILURE;
        if (file.project.empty)
            project = convertProjectName(currentProject);
        else
        {
            project = file.project;
            if (!isValidProjectName(project))
                throw new IllegalFunctionCall("LOAD");
        }
        if (!isValidFilename(file.name))
            throw new IllegalFunctionCall("LOAD");
        auto path = buildPath(projectPath, project, "DAT"w, file.name).to!string;

        //show dialog
        result = DialogResult.SUCCESS;
        import std.stdio;
        import std.algorithm;
        File f = File(path, "rb");
        DataHeader[1] harray;
        f.rawRead(harray);
        if (harray[0].magic != "PCBN0001")
        {
            f.seek(0);
            harray[0].dimension = 1;
            harray[0].dim[] = [cast(int)(f.size / int.sizeof), 0, 0, 0];
            harray[0].type = DataType.int_;
        }
        if (harray[0].dimension != contents.dimCount)
        {
            throw new TypeMismatch("LOAD");
        }
        auto dim = harray[0].dim[0..harray[0].dimension];
        auto len = dim.reduce!((x, y) => x * y);
        T[] result;
        if (harray[0].type == DataType.int_)
        {
            int[] array = new int[len];
            f.rawRead(array);
            static if (!is(T == int))
            {
                result = array.map!(x => x.to!T).array;
            }
            else
            {
                result = array;
            }
        }
        else if (harray[0].type == DataType.double_)
        {
            double[] array = new double[len];
            f.rawRead(array);
            static if (!is(T == double))
            {
                result = array.map!(x => x.to!T).array;
            }
            else
            {
                result = array;
            }
        }
        else if (harray[0].type == DataType.ushort_)
        {
            ushort[] array = new ushort[len];
            f.rawRead(array);
            result = array.map!(x => x.to!T).array;
        }
        else
        {
            throw new TypeMismatch("LOAD(invalid file)");
        }
        //配列に書き込むというより配列を差し替えている
        contents.array = result;
        contents.dim[] = harray[0].dim[];
        contents.dimCount = harray[0].dimension;
        return true;
    }

    void saveTextFile(FileName file, wstring content)
    {
        wstring project;
        result = DialogResult.FAILURE;
        if (file.project.empty)
            project = convertProjectName(currentProject);
        else
        {
            project = file.project;
            if (!isValidProjectName(project))
                throw new IllegalFunctionCall("SAVE");
        }
        if (!isValidFilename(file.name))
            throw new IllegalFunctionCall("SAVE");
        auto path = buildPath(projectPath, project, "TXT"w, file.name).to!string;
        write(path, content.to!string/*UTF8*/);
        result = DialogResult.SUCCESS;
    }

    void saveDataFile(T)(FileName file, Array!T content)
    if (is(T == int) || is(T == double))
    {
        wstring project;
        result = DialogResult.FAILURE;
        if (file.project.empty)
            project = convertProjectName(currentProject);
        else
        {
            project = file.project;
            if (!isValidProjectName(project))
                throw new IllegalFunctionCall("SAVE");
        }
        if (!isValidFilename(file.name))
            throw new IllegalFunctionCall("SAVE");
        auto path = buildPath(projectPath, project, "DAT"w, file.name).to!string;
        DataHeader[1] harray;
        DataHeader* head = harray.ptr;
        static if (is(T == int))
        {
            head.type = DataType.int_;
        }
        else
        {
            head.type = DataType.double_;
        }
        head.dim[] = content.dim[];
        head.dimension = cast(byte)content.dimCount;
        import std.stdio;
        File f = File(path, "wb");
        f.rawWrite(harray);
        f.rawWrite(content.array);
        result = DialogResult.SUCCESS;
    }

    wstring convertProjectName(wstring proj)
    {
        if (proj.empty)
        {
            if (currentProject.empty)
                return "[DEFAULT]";
            return currentProject;
        }
        return proj;
    }

    bool chkfile(wstring path)
    {
        auto file = parseFileName(path);
        if (!Projects.isValidFileName(file.name))
        {
            throw new IllegalFunctionCall("CHKFILE", 1);
        }
        if (!Projects.isValidProjectName(file.project))
        {
            throw new IllegalFunctionCall("CHKFILE", 1);
        }
        if (file.resource == Resource.illegal)
        {
            throw new IllegalFunctionCall("CHKFILE", 1);
        }
        bool isText = file.resource == Resource.none || file.resource == Resource.program || file.resource == Resource.text;
        auto proj = convertProjectName(file.project);
        auto p = buildPath(projectPath, proj, isText ? "TXT"w : "DAT"w, file.name).to!string;
        return p.exists;
    }

    static Resource getResource(wstring resname)
    {
        switch (std.uni.toUpper(resname))
        {
            case "PRG":
                return Resource.program;
            case "GRP":
                return Resource.graphic;
            case "DAT":
                return Resource.data;
            case "TXT":
                return Resource.text;
            default:
                return Resource.illegal;
        }
    }

    static FileName parseFileName(wstring input)
    {
        FileName result;
        auto s = splitResourceName(input);
        if (s[0].length)
        {
            auto resname = s[0];
            auto numind = resname.indexOfAny("1234567890");
            if (numind != -1)
            {
                auto resnum = resname[numind..$].to!int;
                resname = resname[0..numind];
                result.resourceNumber = resnum;
                result.hasResourceNumber = true;
            }
            result.resource = getResource(resname);
        }
        result.project = s[1];
        result.name = s[2];
        return result;
    }
    unittest
    {
        auto a = parseFileName("A");
        assert(a.resource == Resource.none && !a.hasResourceNumber && a.project == "" && a.name == "A");
        a = parseFileName("GRP:B");
        assert(a.resource == Resource.graphic && !a.hasResourceNumber && a.project == "" && a.name == "B");
        a = parseFileName("PRG10:CD");
        assert(a.resource == Resource.program && a.hasResourceNumber && a.resourceNumber == 10 && a.project == "" && a.name == "CD");
        a = parseFileName("DAT:SYS/EF");
        assert(a.resource == Resource.data && !a.hasResourceNumber && a.project == "SYS" && a.name == "EF");
    }
}

enum Resource
{
    none,
    program,
    graphic,
    data,
    text,
    illegal,
}

struct FileName
{
    Resource resource;
    bool hasResourceNumber;
    int resourceNumber;
    wstring project;
    wstring name;
}
