/*
 * Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.termux.shared.file.filesystem;

import android.os.Build;
import android.system.StructStat;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.Set;
import java.util.HashSet;

/**
 * Unix implementation of PosixFileAttributes.
 * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixFileAttributes.java
 */
public class FileAttributes {

    private String filePath;

    private FileDescriptor fileDescriptor;

    private int st_mode;

    private long st_ino;

    private long st_dev;

    private long st_rdev;

    private long st_nlink;

    private int st_uid;

    private int st_gid;

    private long st_size;

    private long st_blksize;

    private long st_blocks;

    private long st_atime_sec;

    private long st_atime_nsec;

    private long st_mtime_sec;

    private long st_mtime_nsec;

    private long st_ctime_sec;

    private long st_ctime_nsec;

    // created lazily
    private volatile String owner;

    private volatile String group;

    private volatile FileKey key;

    private FileAttributes(String filePath) {
        this.filePath = filePath;
    }

    private FileAttributes(FileDescriptor fileDescriptor) {
        this.fileDescriptor = fileDescriptor;
    }

    // get the FileAttributes for a given file
    public static FileAttributes get(String filePath, boolean followLinks) throws IOException {
        FileAttributes fileAttributes;
        if (filePath == null || filePath.isEmpty())
            fileAttributes = new FileAttributes((String) null);
        else
            fileAttributes = new FileAttributes(new File(filePath).getAbsolutePath());
        if (followLinks) {
            NativeDispatcher.stat(filePath, fileAttributes);
        } else {
            NativeDispatcher.lstat(filePath, fileAttributes);
        }
        // Logger.logDebug(fileAttributes.toString());
        return fileAttributes;
    }

    // get the FileAttributes for an open file
    public static FileAttributes get(FileDescriptor fileDescriptor) throws IOException {
        FileAttributes fileAttributes = new FileAttributes(fileDescriptor);
        NativeDispatcher.fstat(fileDescriptor, fileAttributes);
        return fileAttributes;
    }

    public String file() {
        if (filePath != null)
            return filePath;
        else if (fileDescriptor != null)
            return fileDescriptor.toString();
        else
            return null;
    }

    // package-private
    public boolean isSameFile(FileAttributes attrs) {
        return ((st_ino == attrs.st_ino) && (st_dev == attrs.st_dev));
    }

    // package-private
    public int mode() {
        return st_mode;
    }

    public long blksize() {
        return st_blksize;
    }

    public long blocks() {
        return st_blocks;
    }

    public long ino() {
        return st_ino;
    }

    public long dev() {
        return st_dev;
    }

    public long rdev() {
        return st_rdev;
    }

    public long nlink() {
        return st_nlink;
    }

    public int uid() {
        return st_uid;
    }

    public int gid() {
        return st_gid;
    }

    private static FileTime toFileTime(long sec, long nsec) {
        if (nsec == 0) {
            return FileTime.from(sec, TimeUnit.SECONDS);
        } else {
            // truncate to microseconds to avoid overflow with timestamps
            // way out into the future. We can re-visit this if FileTime
            // is updated to define a from(secs,nsecs) method.
            long micro = sec * 1000000L + nsec / 1000L;
            return FileTime.from(micro, TimeUnit.MICROSECONDS);
        }
    }

    public FileTime lastAccessTime() {
        return toFileTime(st_atime_sec, st_atime_nsec);
    }

    public FileTime lastModifiedTime() {
        return toFileTime(st_mtime_sec, st_mtime_nsec);
    }

    public FileTime lastChangeTime() {
        return toFileTime(st_ctime_sec, st_ctime_nsec);
    }

    public FileTime creationTime() {
        return lastModifiedTime();
    }

    public boolean isRegularFile() {
        return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFREG);
    }

    public boolean isDirectory() {
        return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFDIR);
    }

    public boolean isSymbolicLink() {
        return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFLNK);
    }

    public boolean isCharacter() {
        return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFCHR);
    }

    public boolean isFifo() {
        return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFIFO);
    }

    public boolean isSocket() {
        return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFSOCK);
    }

    public boolean isBlock() {
        return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFBLK);
    }

    public boolean isOther() {
        int type = st_mode & UnixConstants.S_IFMT;
        return (type != UnixConstants.S_IFREG && type != UnixConstants.S_IFDIR && type != UnixConstants.S_IFLNK);
    }

    public boolean isDevice() {
        int type = st_mode & UnixConstants.S_IFMT;
        return (type == UnixConstants.S_IFCHR || type == UnixConstants.S_IFBLK || type == UnixConstants.S_IFIFO);
    }

    public long size() {
        return st_size;
    }

    public FileKey fileKey() {
        if (key == null) {
            synchronized (this) {
                if (key == null) {
                    key = new FileKey(st_dev, st_ino);
                }
            }
        }
        return key;
    }

    public String owner() {
        if (owner == null) {
            synchronized (this) {
                if (owner == null) {
                    owner = Integer.toString(st_uid);
                }
            }
        }
        return owner;
    }

    public String group() {
        if (group == null) {
            synchronized (this) {
                if (group == null) {
                    group = Integer.toString(st_gid);
                }
            }
        }
        return group;
    }

    public Set<FilePermission> permissions() {
        int bits = (st_mode & UnixConstants.S_IAMB);
        HashSet<FilePermission> perms = new HashSet<>();
        if ((bits & UnixConstants.S_IRUSR) > 0)
            perms.add(FilePermission.OWNER_READ);
        if ((bits & UnixConstants.S_IWUSR) > 0)
            perms.add(FilePermission.OWNER_WRITE);
        if ((bits & UnixConstants.S_IXUSR) > 0)
            perms.add(FilePermission.OWNER_EXECUTE);
        if ((bits & UnixConstants.S_IRGRP) > 0)
            perms.add(FilePermission.GROUP_READ);
        if ((bits & UnixConstants.S_IWGRP) > 0)
            perms.add(FilePermission.GROUP_WRITE);
        if ((bits & UnixConstants.S_IXGRP) > 0)
            perms.add(FilePermission.GROUP_EXECUTE);
        if ((bits & UnixConstants.S_IROTH) > 0)
            perms.add(FilePermission.OTHERS_READ);
        if ((bits & UnixConstants.S_IWOTH) > 0)
            perms.add(FilePermission.OTHERS_WRITE);
        if ((bits & UnixConstants.S_IXOTH) > 0)
            perms.add(FilePermission.OTHERS_EXECUTE);
        return perms;
    }

    public void loadFromStructStat(StructStat structStat) {
        this.st_mode = structStat.st_mode;
        this.st_ino = structStat.st_ino;
        this.st_dev = structStat.st_dev;
        this.st_rdev = structStat.st_rdev;
        this.st_nlink = structStat.st_nlink;
        this.st_uid = structStat.st_uid;
        this.st_gid = structStat.st_gid;
        this.st_size = structStat.st_size;
        this.st_blksize = structStat.st_blksize;
        this.st_blocks = structStat.st_blocks;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
            this.st_atime_sec = structStat.st_atim.tv_sec;
            this.st_atime_nsec = structStat.st_atim.tv_nsec;
            this.st_mtime_sec = structStat.st_mtim.tv_sec;
            this.st_mtime_nsec = structStat.st_mtim.tv_nsec;
            this.st_ctime_sec = structStat.st_ctim.tv_sec;
            this.st_ctime_nsec = structStat.st_ctim.tv_nsec;
        } else {
            this.st_atime_sec = structStat.st_atime;
            this.st_atime_nsec = 0;
            this.st_mtime_sec = structStat.st_mtime;
            this.st_mtime_nsec = 0;
            this.st_ctime_sec = structStat.st_ctime;
            this.st_ctime_nsec = 0;
        }
    }

    public String getFileString() {
        return "File: `" + file() + "`";
    }

    public String getTypeString() {
        return "Type: `" + FileTypes.getFileType(this).getName() + "`";
    }

    public String getSizeString() {
        return "Size: `" + size() + "`";
    }

    public String getBlocksString() {
        return "Blocks: `" + blocks() + "`";
    }

    public String getIOBlockString() {
        return "IO Block: `" + blksize() + "`";
    }

    public String getDeviceString() {
        return "Device: `" + Long.toHexString(st_dev) + "`";
    }

    public String getInodeString() {
        return "Inode: `" + st_ino + "`";
    }

    public String getLinksString() {
        return "Links: `" + nlink() + "`";
    }

    public String getDeviceTypeString() {
        return "Device Type: `" + rdev() + "`";
    }

    public String getOwnerString() {
        return "Owner: `" + owner() + "`";
    }

    public String getGroupString() {
        return "Group: `" + group() + "`";
    }

    public String getPermissionString() {
        return "Permissions: `" + FilePermissions.toString(permissions()) + "`";
    }

    public String getAccessTimeString() {
        return "Access Time: `" + lastAccessTime() + "`";
    }

    public String getModifiedTimeString() {
        return "Modified Time: `" + lastModifiedTime() + "`";
    }

    public String getChangeTimeString() {
        return "Change Time: `" + lastChangeTime() + "`";
    }

    @NonNull
    @Override
    public String toString() {
        return getFileAttributesLogString(this);
    }

    public static String getFileAttributesLogString(final FileAttributes fileAttributes) {
        if (fileAttributes == null)
            return "null";
        StringBuilder logString = new StringBuilder();
        logString.append(fileAttributes.getFileString());
        logString.append("\n").append(fileAttributes.getTypeString());
        logString.append("\n").append(fileAttributes.getSizeString());
        logString.append("\n").append(fileAttributes.getBlocksString());
        logString.append("\n").append(fileAttributes.getIOBlockString());
        logString.append("\n").append(fileAttributes.getDeviceString());
        logString.append("\n").append(fileAttributes.getInodeString());
        logString.append("\n").append(fileAttributes.getLinksString());
        if (fileAttributes.isBlock() || fileAttributes.isCharacter())
            logString.append("\n").append(fileAttributes.getDeviceTypeString());
        logString.append("\n").append(fileAttributes.getOwnerString());
        logString.append("\n").append(fileAttributes.getGroupString());
        logString.append("\n").append(fileAttributes.getPermissionString());
        logString.append("\n").append(fileAttributes.getAccessTimeString());
        logString.append("\n").append(fileAttributes.getModifiedTimeString());
        logString.append("\n").append(fileAttributes.getChangeTimeString());
        return logString.toString();
    }
}
