/**
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *  
 *    http://www.apache.org/licenses/LICENSE-2.0
 *  
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License. 
 *  
 */
package org.apache.kerby.kerberos.kerb.ccache;

import org.apache.kerby.kerberos.kerb.type.base.PrincipalName;
import org.apache.kerby.kerberos.kerb.type.ticket.SgtTicket;
import org.apache.kerby.kerberos.kerb.type.ticket.TgtTicket;
import org.apache.kerby.kerberos.kerb.type.ticket.Ticket;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;

public class CredentialCache implements KrbCredentialCache {
    public static final int FCC_FVNO_1 = 0x501;
    public static final int FCC_FVNO_2 = 0x502;
    public static final int FCC_FVNO_3 = 0x503;
    public static final int FCC_FVNO_4 = 0x504;

    public static final int FCC_TAG_DELTATIME = 1;
    private final List<Credential> credentials;
    private int version = FCC_FVNO_4;
    private List<Tag> tags;
    private PrincipalName primaryPrincipal;

    public CredentialCache() {
        credentials = new ArrayList<>();
    }

    public CredentialCache(TgtTicket tgt) {
        this();
        addCredential(new Credential(tgt));
        setPrimaryPrincipal(tgt.getClientPrincipal());
    }

    public CredentialCache(SgtTicket sgt) {
        this();
        addCredential(new Credential(sgt, sgt.getClientPrincipal()));
        setPrimaryPrincipal(sgt.getClientPrincipal());
    }

    public CredentialCache(Credential credential) {
        this();
        addCredential(credential);
        setPrimaryPrincipal(credential.getClientName());
    }

    public static void main(String[] args) throws IOException {
        if (args.length != 2) {
            System.err.println("Dump credential cache file");
            System.err.println("Usage: CredentialCache <ccache-file>");
            System.exit(1);
        }

        String cacheFile = args[1];
        CredentialCache cc = new CredentialCache();
        cc.load(new File(cacheFile));

        for (Credential cred : cc.getCredentials()) {
            Ticket tkt = cred.getTicket();
            System.out.println("Tkt server name: " + tkt.getSname().getName());
            System.out.println("Tkt client name: " + cred.getClientName().getName());
            System.out.println("Tkt encrypt type: " + tkt.getEncryptedEncPart().getEType().getName());
        }
    }

    @Override
    public void store(File ccacheFile) throws IOException {
        try (OutputStream outputStream = Files.newOutputStream(ccacheFile.toPath())) {
            store(outputStream);
        }
    }

    @Override
    public void store(OutputStream outputStream) throws IOException {
        if (outputStream == null) {
            throw new IllegalArgumentException("Invalid and null output stream");
        }

        CredCacheOutputStream ccos = new CredCacheOutputStream(outputStream);

        doStore(ccos);

        ccos.close();
    }

    private void doStore(CredCacheOutputStream ccos) throws IOException {
        this.version = FCC_FVNO_3;

        writeVersion(ccos);

        if (version == FCC_FVNO_4) {
            writeTags(ccos);
        }

        ccos.writePrincipal(primaryPrincipal, version);

        for (Credential cred : credentials) {
            cred.store(ccos, version);
        }
    }

    @Override
    public PrincipalName getPrimaryPrincipal() {
        return primaryPrincipal;
    }

    @Override
    public void setPrimaryPrincipal(PrincipalName principal) {
        primaryPrincipal = principal;
    }

    @Override
    public int getVersion() {
        return version;
    }

    @Override
    public void setVersion(int version) {
        this.version = version;
    }

    public List<Tag> getTags() {
        return this.tags;
    }

    public void setTags(List<Tag> tags) {
        this.tags = tags;
    }

    @Override
    public List<Credential> getCredentials() {
        return credentials;
    }

    @Override
    public void addCredential(Credential credential) {
        if (credential != null) {
            for (Credential cred : this.credentials) {
                if (cred.getServerName().getName().equals(credential.getServerName().getName())) {
                    return;
                }
            }
            this.credentials.add(credential);
        }
    }

    @Override
    public void addCredentials(List<Credential> credentials) {
        if (credentials != null) {
            this.credentials.addAll(credentials);
        }
    }

    @Override
    public void removeCredentials(List<Credential> credentials) {
        if (credentials != null) {
            for (Credential cred : credentials) {
                removeCredential(cred);
            }
        }
    }

    @Override
    public void removeCredential(Credential credential) {
        if (credential != null) {
            for (Credential cred : credentials) {
                if (cred.equals(credential)) {
                    credentials.remove(cred);
                    break;
                }
            }
        }
    }

    @Override
    public void load(File ccacheFile) throws IOException {
        if (!ccacheFile.exists() || !ccacheFile.canRead()) {
            throw new IllegalArgumentException("Invalid ccache file: "
                    + ccacheFile.getAbsolutePath());
        }

        try (InputStream inputStream = Files.newInputStream(ccacheFile.toPath())) {
            load(inputStream);
        }
    }

    @Override
    public void load(InputStream inputStream) throws IOException {
        if (inputStream == null) {
            throw new IllegalArgumentException("Invalid and null input stream");
        }

        CredCacheInputStream ccis = new CredCacheInputStream(inputStream);

        doLoad(ccis);

        ccis.close();
    }

    private void doLoad(CredCacheInputStream ccis) throws IOException {
        this.version = readVersion(ccis);
        if (version == FCC_FVNO_4) {
            this.tags = readTags(ccis);
        }

        this.primaryPrincipal = ccis.readPrincipal(version);

        this.credentials.addAll(readCredentials(ccis));
    }

    private List<Credential> readCredentials(CredCacheInputStream ccis)
            throws IOException {
        List<Credential> results = new ArrayList<Credential>(2);

        Credential cred;
        while (ccis.available() > 0) {
            cred = new Credential();
            cred.load(ccis, version);

            results.add(cred);
        }

        return results;
    }

    private int readVersion(CredCacheInputStream ccis) throws IOException {
        int result = ccis.readShort();
        return result;
    }

    private List<Tag> readTags(CredCacheInputStream ccis) throws IOException {
        int len = ccis.readShort();
        List<Tag> tags = new ArrayList<Tag>();

        int tag, tagLen, time, usec;
        while (len > 0) {
            tag = ccis.readShort();
            tagLen = ccis.readShort();
            switch (tag) {
                case FCC_TAG_DELTATIME:
                    time = ccis.readInt();
                    usec = ccis.readInt();
                    tags.add(new Tag(tag, time, usec));
                    break;
                default:
                    if (ccis.read(new byte[tagLen], 0, tagLen) == -1) { // ignore unknown tag
                        throw new IOException();
                    }
            }
            len = len - (4 + tagLen);
        }

        return tags;
    }

    private void writeVersion(CredCacheOutputStream ccos) throws IOException {
        ccos.writeShort(version);
    }

    private void writeTags(CredCacheOutputStream ccos) throws IOException {
        if (tags == null) {
            ccos.writeShort(0);
            return;
        }

        int length = 0;
        for (Tag tag : tags) {
            if (tag.tag != FCC_TAG_DELTATIME) {
                continue;
            }
            length += tag.length;
        }
        ccos.writeShort(length);

        for (Tag tag : tags) {
            if (tag.tag != CredentialCache.FCC_TAG_DELTATIME) {
                continue;
            }
            writeTag(ccos, tag);
        }
    }

    private void writeTag(CredCacheOutputStream ccos, Tag tag) throws IOException {
        ccos.writeShort(tag.tag);
        ccos.writeShort(tag.length);
        ccos.writeInt(tag.time);
        ccos.writeInt(tag.usec);
    }
}
