package com.jscape.inet.sftp.protocol.v4.marshaling;

import com.jscape.inet.sftp.protocol.v4.messages.Acl;
import com.jscape.inet.sftp.protocol.v4.messages.FileAttributeFlags;
import com.jscape.inet.sftp.protocol.v4.messages.FileAttributes;
import com.jscape.inet.ssh.protocol.v2.marshaling.primitive.StringCodec;
import com.jscape.inet.ssh.protocol.v2.marshaling.primitive.Uint32Codec;
import com.jscape.inet.ssh.protocol.v2.marshaling.primitive.Uint64Codec;
import com.jscape.util.file.PosixFilePermissions;
import com.jscape.util.file.PosixFilePermissionsFormat;
import com.jscape.util.io.ByteArrayInputStream;
import com.jscape.util.io.ByteArrayOutputStream;
import com.jscape.util.io.primitive.ByteCodec;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author Alex Usun
 */
public class FileAttributesCodec {

    public static FileAttributes readValue(InputStream in)
            throws IOException {
        FileAttributeFlags flags = FileAttributeFlagsFormat.parse(Uint32Codec.readValue(in));
        int type = ByteCodec.readValue(in) & 0xFF;
        Long size = flags.sizeAttributePresent ? Uint64Codec.readValue(in) : null;
        String owner = flags.ownerGroupAttributesPresent ? StringCodec.readUtf8Value(in) : null;
        String group = flags.ownerGroupAttributesPresent ? StringCodec.readUtf8Value(in) : null;
        PosixFilePermissions permissions = flags.permissionsAttributePresent ? PosixFilePermissionsFormat.parse(Uint32Codec.readValue(in)) : null;
        Long accessTime = flags.accessTimeAttributePresent ? Uint64Codec.readValue(in) : null;
        Integer accessTimeNanos = flags.accessTimeAttributePresent && flags.subsecondTimesPresent ? Uint32Codec.readValue(in) : null;
        Long createTime = flags.createTimeAttributePresent ? Uint64Codec.readValue(in) : null;
        Integer createTimeNanos = flags.createTimeAttributePresent && flags.subsecondTimesPresent ? Uint32Codec.readValue(in) : null;
        Long modificationTime = flags.modifyTimeAttributePresent ? Uint64Codec.readValue(in) : null;
        Integer modificationTimeNanos = flags.modifyTimeAttributePresent && flags.subsecondTimesPresent ? Uint32Codec.readValue(in) : null;
        Acl[] acls = flags.aclAttributePresent ? readAcls(StringCodec.readValue(in)) : null;
        Map<String, String> extendedData = flags.extendedAttributesPresent ? readExtendedData(in) : new HashMap<String, String>();

        return new FileAttributes(type,
                                  size,
                                  owner,
                                  group,
                                  permissions,
                                  accessTime,
                                  accessTimeNanos,
                                  createTime,
                                  createTimeNanos,
                                  modificationTime,
                                  modificationTimeNanos,
                                  acls,
                                  extendedData);
    }

    public static void writeValue(FileAttributes value, OutputStream out)
            throws IOException {
        FileAttributeFlags flags = FileAttributeFlags.flagsFor(value);
        Uint32Codec.writeValue(FileAttributeFlagsFormat.format(flags), out);
        ByteCodec.writeValue((byte) value.type, out);

        if (flags.sizeAttributePresent) {
            Uint64Codec.writeValue(value.size, out);
        }

        if (flags.ownerGroupAttributesPresent) {
            StringCodec.writeUtf8Value(value.owner, out);
            StringCodec.writeUtf8Value(value.group, out);
        }

        if (flags.permissionsAttributePresent) {
            Uint32Codec.writeValue(PosixFilePermissionsFormat.format(value.permissions), out);
        }

        if (flags.accessTimeAttributePresent) {
            Uint64Codec.writeValue(value.accessTime, out);
        }

        if (flags.createTimeAttributePresent) {
            Uint64Codec.writeValue(value.createTime, out);
        }

        if (flags.modifyTimeAttributePresent) {
            Uint64Codec.writeValue(value.modificationTime, out);
        }

        if (flags.aclAttributePresent) {
            StringCodec.writeValue(writeAcls(value.acls), out);
        }

        if (flags.extendedAttributesPresent) {
            writeExtendedData(value.extendedData, out);
        }
    }

    private static Acl[] readAcls(byte[] data)
            throws IOException {
        ByteArrayInputStream in = new ByteArrayInputStream(data);
        int count = Uint32Codec.readValue(in);
        Acl[] acls = new Acl[count];
        for (int i = 0; i < acls.length; ++i) {
            acls[i] = readAcl(in);
        }

        return acls;
    }

    private static byte[] writeAcls(Acl[] acls)
            throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        Uint32Codec.writeValue(acls.length, out);
        for (Acl acl : acls) {
            writeAcl(acl, out);
        }

        return out.toByteArray();
    }

    private static Acl readAcl(ByteArrayInputStream in)
            throws IOException {
        int type = Uint32Codec.readValue(in);
        Acl.Flag flag = parseFlag(Uint32Codec.readValue(in));
        Acl.Mask mask = parseMask(Uint32Codec.readValue(in));
        String who = StringCodec.readUtf8Value(in);

        return new Acl(type, flag, mask, who);
    }

    private static void writeAcl(Acl value, ByteArrayOutputStream out)
            throws IOException {
        Uint32Codec.writeValue(value.type, out);
        Uint32Codec.writeValue(formatFlag(value.flag), out);
        Uint32Codec.writeValue(formatMask(value.mask), out);
        StringCodec.writeUtf8Value(value.who, out);
    }

    private static Acl.Flag parseFlag(int data) {
        return new Acl.Flag(AclFlagMask.ACE4_FILE_INHERIT_ACE.presentIn(data),
                            AclFlagMask.ACE4_DIRECTORY_INHERIT_ACE.presentIn(data),
                            AclFlagMask.ACE4_NO_PROPAGATE_INHERIT_ACE.presentIn(data),
                            AclFlagMask.ACE4_INHERIT_ONLY_ACE.presentIn(data),
                            AclFlagMask.ACE4_SUCCESSFUL_ACCESS_ACE_FLAG.presentIn(data),
                            AclFlagMask.ACE4_FAILED_ACCESS_ACE_FLAG.presentIn(data),
                            AclFlagMask.ACE4_IDENTIFIER_GROUP.presentIn(data));
    }

    private static int formatFlag(Acl.Flag flag) {
        int data = 0;
        data = AclFlagMask.ACE4_FILE_INHERIT_ACE.set(flag.fileInheritAce, data);
        data = AclFlagMask.ACE4_DIRECTORY_INHERIT_ACE.set(flag.directoryInheritAce, data);
        data = AclFlagMask.ACE4_NO_PROPAGATE_INHERIT_ACE.set(flag.noPropagateInheritAce, data);
        data = AclFlagMask.ACE4_INHERIT_ONLY_ACE.set(flag.inheritOnlyAce, data);
        data = AclFlagMask.ACE4_SUCCESSFUL_ACCESS_ACE_FLAG.set(flag.successfulAccessAceFlag, data);
        data = AclFlagMask.ACE4_FAILED_ACCESS_ACE_FLAG.set(flag.failedAccessAceFlag, data);
        data = AclFlagMask.ACE4_IDENTIFIER_GROUP.set(flag.identifierGroup, data);

        return data;
    }

    private static Acl.Mask parseMask(int data) {
        return new Acl.Mask(AclMask.ACE4_READ_DATA.presentIn(data),
                            AclMask.ACE4_LIST_DIRECTORY.presentIn(data),
                            AclMask.ACE4_WRITE_DATA.presentIn(data),
                            AclMask.ACE4_ADD_FILE.presentIn(data),
                            AclMask.ACE4_APPEND_DATA.presentIn(data),
                            AclMask.ACE4_ADD_SUBDIRECTORY.presentIn(data),
                            AclMask.ACE4_READ_NAMED_ATTRS.presentIn(data),
                            AclMask.ACE4_WRITE_NAMED_ATTRS.presentIn(data),
                            AclMask.ACE4_EXECUTE.presentIn(data),
                            AclMask.ACE4_DELETE_CHILD.presentIn(data),
                            AclMask.ACE4_READ_ATTRIBUTES.presentIn(data),
                            AclMask.ACE4_WRITE_ATTRIBUTES.presentIn(data),
                            AclMask.ACE4_DELETE.presentIn(data),
                            AclMask.ACE4_READ_ACL.presentIn(data),
                            AclMask.ACE4_WRITE_ACL.presentIn(data),
                            AclMask.ACE4_WRITE_OWNER.presentIn(data),
                            AclMask.ACE4_SYNCHRONIZE.presentIn(data));
    }

    private static int formatMask(Acl.Mask value) {
        int data = 0;
        data = AclMask.ACE4_READ_DATA.set(value.readData, data);
        data = AclMask.ACE4_LIST_DIRECTORY.set(value.listDirectory, data);
        data = AclMask.ACE4_WRITE_DATA.set(value.writeData, data);
        data = AclMask.ACE4_ADD_FILE.set(value.addFile, data);
        data = AclMask.ACE4_APPEND_DATA.set(value.appendData, data);
        data = AclMask.ACE4_ADD_SUBDIRECTORY.set(value.addSubdirectory, data);
        data = AclMask.ACE4_READ_NAMED_ATTRS.set(value.readNamedAttrs, data);
        data = AclMask.ACE4_WRITE_NAMED_ATTRS.set(value.writeNamedAttrs, data);
        data = AclMask.ACE4_EXECUTE.set(value.execute, data);
        data = AclMask.ACE4_DELETE_CHILD.set(value.deleteChild, data);
        data = AclMask.ACE4_READ_ATTRIBUTES.set(value.readAttributes, data);
        data = AclMask.ACE4_WRITE_ATTRIBUTES.set(value.writeAttributes, data);
        data = AclMask.ACE4_DELETE.set(value.delete, data);
        data = AclMask.ACE4_READ_ACL.set(value.readAcl, data);
        data = AclMask.ACE4_WRITE_ACL.set(value.writeAcl, data);
        data = AclMask.ACE4_WRITE_OWNER.set(value.writeOwner, data);
        data = AclMask.ACE4_SYNCHRONIZE.set(value.synchronize, data);

        return data;
    }

    private static Map<String, String> readExtendedData(InputStream in)
            throws IOException {
        int count = Uint32Codec.readValue(in);
        Map<String, String> extendedData = new LinkedHashMap<>();
        while (count-- > 0) {
            String name = StringCodec.readUtf8Value(in);
            String data = StringCodec.readUtf8Value(in);
            extendedData.put(name, data);
        }

        return extendedData;
    }

    private static void writeExtendedData(Map<String, String> extendedData, OutputStream out)
            throws IOException {
        Uint32Codec.writeValue(extendedData.size(), out);
        for (Map.Entry<String, String> entry : extendedData.entrySet()) {
            StringCodec.writeUtf8Value(entry.getKey(), out);
            StringCodec.writeUtf8Value(entry.getValue(), out);
        }
    }

    private FileAttributesCodec() {
    }

    public enum AclFlagMask {

        ACE4_FILE_INHERIT_ACE(0x00000001),
        ACE4_DIRECTORY_INHERIT_ACE(0x00000002),
        ACE4_NO_PROPAGATE_INHERIT_ACE(0x00000004),
        ACE4_INHERIT_ONLY_ACE(0x00000008),
        ACE4_SUCCESSFUL_ACCESS_ACE_FLAG(0x00000010),
        ACE4_FAILED_ACCESS_ACE_FLAG(0x00000020),
        ACE4_IDENTIFIER_GROUP(0x00000040);

        public final int code;

        AclFlagMask(int code) {
            this.code = code;
        }

        public boolean presentIn(int flags) {
            return (flags & this.code) == this.code;
        }

        public int set(boolean value, int flags) {
            return value ? flags | this.code : flags;
        }
    }

    public enum AclMask {

        ACE4_READ_DATA(0x00000001),
        ACE4_LIST_DIRECTORY(0x00000001),
        ACE4_WRITE_DATA(0x00000002),
        ACE4_ADD_FILE(0x00000002),
        ACE4_APPEND_DATA(0x00000004),
        ACE4_ADD_SUBDIRECTORY(0x00000004),
        ACE4_READ_NAMED_ATTRS(0x00000008),
        ACE4_WRITE_NAMED_ATTRS(0x00000010),
        ACE4_EXECUTE(0x00000020),
        ACE4_DELETE_CHILD(0x00000040),
        ACE4_READ_ATTRIBUTES(0x00000080),
        ACE4_WRITE_ATTRIBUTES(0x00000100),
        ACE4_DELETE(0x00010000),
        ACE4_READ_ACL(0x00020000),
        ACE4_WRITE_ACL(0x00040000),
        ACE4_WRITE_OWNER(0x00080000),
        ACE4_SYNCHRONIZE(0x00100000);

        public final int code;

        AclMask(int code) {
            this.code = code;
        }

        public boolean presentIn(int flags) {
            return (flags & this.code) == this.code;
        }

        public int set(boolean value, int flags) {
            return value ? flags | this.code : flags;
        }
    }

}
