/*******************************************************************************
 * Copyright (c) 2015, 2016 IBH SYSTEMS GmbH.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *******************************************************************************/
package org.eclipse.packagedrone.repo.channel.impl.transfer;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.eclipse.packagedrone.VersionInformation;
import org.eclipse.packagedrone.repo.MetaKey;
import org.eclipse.packagedrone.repo.channel.ArtifactInformation;
import org.eclipse.packagedrone.repo.channel.ChannelDetails;
import org.eclipse.packagedrone.repo.channel.ChannelId;
import org.eclipse.packagedrone.repo.channel.ChannelService;
import org.eclipse.packagedrone.repo.channel.ChannelService.By;
import org.eclipse.packagedrone.repo.channel.DescriptorAdapter;
import org.eclipse.packagedrone.repo.channel.ModifiableChannel;
import org.eclipse.packagedrone.repo.channel.ReadableChannel;
import org.eclipse.packagedrone.repo.channel.transfer.ImportOptions;
import org.eclipse.packagedrone.repo.channel.transfer.TransferService;
import org.eclipse.packagedrone.repo.trigger.TriggeredChannel;
import org.eclipse.packagedrone.repo.xml.XmlHelper;
import org.eclipse.packagedrone.utils.xml.XmlToolsFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class TransferServiceImpl implements TransferService
{
    private ChannelService channelService;

    private XmlToolsFactory xmlToolsFactory;

    public void setChannelService ( final ChannelService channelService )
    {
        this.channelService = channelService;
    }

    public void setXmlToolsFactory ( final XmlToolsFactory xmlToolsFactory )
    {
        this.xmlToolsFactory = xmlToolsFactory;
    }

    private class Entry
    {
        private final Map<String, Entry> children = new HashMap<> ();

        private ZipEntry zipEntry;

        private final List<String> ids;

        public Entry ()
        {
            this.ids = Collections.emptyList ();
        }

        public Entry ( final List<String> ids )
        {
            this.ids = ids;
        }

        public void addChild ( final LinkedList<String> path, final ZipEntry zipEntry )
        {
            final String seg = path.pop ();

            Entry child = this.children.get ( seg );
            if ( child == null )
            {
                final List<String> ids = new ArrayList<> ( this.ids );
                ids.add ( seg );
                child = new Entry ( ids );
                this.children.put ( seg, child );
            }

            if ( path.isEmpty () )
            {
                child.zipEntry = zipEntry;
            }
            else
            {
                child.addChild ( path, zipEntry );
            }
        }

        public void store ( final ZipFile zip, final ModifiableChannel channel, final Optional<ArtifactInformation> parent ) throws Exception
        {
            for ( final Entry child : this.children.values () )
            {
                final String baseName = String.format ( "%s%s/", "artifacts/", child.ids.stream ().collect ( Collectors.joining ( "/" ) ) );

                final String name = getData ( zip, baseName + "name" );

                final String generatorId = getData ( zip, baseName + "generator" );

                final ArtifactInformation result;
                try ( final InputStream stream = zip.getInputStream ( child.zipEntry ) )
                {
                    final Map<MetaKey, String> providedMetaData = getProperties ( zip, baseName + "properties.xml" );

                    if ( generatorId != null )
                    {
                        result = channel.getContext ().createGeneratorArtifact ( generatorId, stream, name, providedMetaData );
                    }
                    else
                    {
                        final String parentId = parent.map ( ArtifactInformation::getId ).orElse ( null );
                        result = channel.getContext ().createArtifact ( parentId, stream, name, providedMetaData );
                    }
                }

                if ( result != null )
                {
                    // only add children if we did not get a veto
                    child.store ( zip, channel, Optional.of ( result ) );
                }
            }
        }
    }

    @Override
    public void importAll ( final InputStream inputStream, final ImportOptions options, final boolean wipe ) throws IOException
    {
        ZipEntry ze;

        if ( wipe )
        {
            this.channelService.wipeClean ();
        }

        final ZipInputStream zis = new ZipInputStream ( inputStream );
        while ( ( ze = zis.getNextEntry () ) != null )
        {
            if ( ze.isDirectory () )
            {
                continue;
            }

            final String name = ze.getName ();
            if ( !name.endsWith ( ".zip" ) )
            {
                continue;
            }

            importChannel ( zis, options );
        }
    }

    @Override
    public ChannelId importChannel ( final InputStream inputStream, final ImportOptions options ) throws IOException
    {
        final Path tmp = Files.createTempFile ( "imp", null );
        try
        {
            try ( OutputStream tmpStream = new BufferedOutputStream ( new FileOutputStream ( tmp.toFile () ) ) )
            {
                ByteStreams.copy ( inputStream, tmpStream );
            }

            return processImport ( tmp, options );
        }
        finally
        {
            Files.deleteIfExists ( tmp );
        }
    }

    private ChannelId processImport ( final Path tmp, final ImportOptions options ) throws IOException
    {
        try ( final ZipFile zip = new ZipFile ( tmp.toFile () ) )
        {
            final String version = getData ( zip, "version" );

            if ( !"1".equals ( version ) )
            {
                throw new IllegalArgumentException ( String.format ( "Version '%s' is not supported", version ) );
            }

            // read basic channel data

            final String name = getData ( zip, "name" );
            final Set<String> names = parseNames ( getData ( zip, "names" ) );

            if ( name != null )
            {
                names.add ( name );
            }

            final String description = getData ( zip, "description" );
            final Map<MetaKey, String> properties = getProperties ( zip, "properties.xml" );
            final Set<String> aspects = getAspects ( zip );

            // create the channel

            final ChannelDetails details = new ChannelDetails ();
            details.setDescription ( description );
            final ChannelId channelId = this.channelService.create ( "apm", details, Collections.emptyMap () );

            final By by = By.id ( channelId.getId () );

            // apply the name if required and present

            if ( options.isUseNames () && !names.isEmpty () )
            {
                this.channelService.accessRun ( by, DescriptorAdapter.class, channel -> {
                    channel.setNames ( names );
                } );
            }

            this.channelService.accessRun ( by, ModifiableChannel.class, channel -> {

                // set provided meta data

                if ( properties != null && !properties.isEmpty () )
                {
                    channel.applyMetaData ( properties );
                }

                // process artifacts

                processArtifacts ( channel, zip );

                // finally enable the aspects

                if ( aspects != null && !aspects.isEmpty () )
                {
                    channel.getContext ().addAspects ( aspects );
                }

            } );

            final String triggers = getData ( zip, "triggers.json" );
            if ( triggers != null && options.isProcessTriggers () )
            {
                final TriggerExport triggerExport = TriggerExport.fromJson ( triggers );
                this.channelService.accessRun ( by, TriggeredChannel.class, channel -> {
                    importTriggers ( triggerExport, channel );
                } );
            }

            return channelId;
        }
    }

    public void processArtifacts ( final ModifiableChannel channel, final ZipFile zip ) throws IOException
    {
        // first gather a artifacts

        final Enumeration<? extends ZipEntry> entries = zip.entries ();

        final Entry root = new Entry ();
        while ( entries.hasMoreElements () )
        {
            final ZipEntry ze = entries.nextElement ();

            final String name = ze.getName ();
            if ( name.startsWith ( "artifacts/" ) && name.endsWith ( "/data" ) )
            {
                final List<String> segs = Arrays.asList ( name.split ( "\\/" ) );
                root.addChild ( new LinkedList<> ( segs.subList ( 1, segs.size () - 1 ) ), ze );
            }
        }

        // now import them hierarchically

        try
        {
            root.store ( zip, channel, Optional.empty () );
        }
        catch ( final Exception e )
        {
            throw new IOException ( "Failed to import artifacts", e );
        }
    }

    private Map<MetaKey, String> getProperties ( final ZipFile zip, final String name ) throws IOException
    {
        final ZipEntry ze = zip.getEntry ( name );
        if ( ze == null )
        {
            return null;
        }

        try ( InputStream stream = zip.getInputStream ( ze ) )
        {
            return readProperties ( stream );
        }
    }

    private String getData ( final ZipFile zip, final String name ) throws IOException
    {
        final ZipEntry ze = zip.getEntry ( name );
        if ( ze == null )
        {
            return null;
        }

        try ( Reader reader = new InputStreamReader ( zip.getInputStream ( ze ), StandardCharsets.UTF_8 ) )
        {
            return CharStreams.toString ( reader );
        }
    }

    /**
     * Read in a map of properties
     *
     * @param stream
     *            input stream
     * @return the map of properties
     * @throws IOException
     *             if anything goes wrong reading the file
     */
    private Map<MetaKey, String> readProperties ( final InputStream stream ) throws IOException
    {
        try
        {
            // wrap the input stream since we don't want the XML parser to close the stream while parsing

            final Document doc = this.xmlToolsFactory.newDocumentBuilder ().parse ( new FilterInputStream ( stream ) {
                @Override
                public void close ()
                {
                    // do nothing
                }
            } );

            final Element root = doc.getDocumentElement ();
            if ( !"properties".equals ( root.getNodeName () ) )
            {
                throw new IllegalStateException ( String.format ( "Root element must be of type '%s'", "properties" ) );
            }

            final Map<MetaKey, String> result = new HashMap<> ();

            for ( final Element ele : XmlHelper.iterElement ( root, "property" ) )
            {
                final String namespace = ele.getAttribute ( "namespace" );
                final String key = ele.getAttribute ( "key" );
                final String value = ele.getTextContent ();

                if ( namespace.isEmpty () || key.isEmpty () )
                {
                    continue;
                }

                result.put ( new MetaKey ( namespace, key ), value );
            }

            return result;
        }
        catch ( final Exception e )
        {
            throw new IOException ( "Failed to read properties", e );
        }
    }

    private Set<String> getAspects ( final ZipFile zip ) throws IOException
    {
        final ZipEntry ze = zip.getEntry ( "aspects" );
        if ( ze == null )
        {
            return Collections.emptySet ();
        }

        try ( InputStream stream = zip.getInputStream ( ze ) )
        {
            final List<String> lines = CharStreams.readLines ( new InputStreamReader ( stream, StandardCharsets.UTF_8 ) );
            return new HashSet<> ( lines );
        }
    }

    protected void initExportFile ( final ZipOutputStream zos ) throws IOException
    {
        putDataEntry ( zos, "version", "1" );
        putDataEntry ( zos, "droneVersion", VersionInformation.VERSION );
    }

    @Override
    public void exportAll ( final OutputStream stream ) throws IOException
    {
        final ZipOutputStream zos = new ZipOutputStream ( stream );

        initExportFile ( zos );

        final Collection<? extends ChannelId> ids = this.channelService.list ();

        // TODO: run exportAll inside a channel service lock

        for ( final ChannelId channelId : ids )
        {
            zos.putNextEntry ( new ZipEntry ( String.format ( "%s.zip", channelId.getId () ) ) );
            exportChannel ( By.id ( channelId.getId () ), zos );
            zos.closeEntry ();
        }
        zos.finish ();
    }

    /**
     * Export the content of a channel
     *
     * @param by
     *            the channel to export
     * @param stream
     *            the stream to write the export file to
     * @throws IOException
     *             if the export cannot be performed
     */
    private void exportChannel ( final By by, final OutputStream stream ) throws IOException
    {
        final ZipOutputStream zos = new ZipOutputStream ( stream );

        initExportFile ( zos );

        this.channelService.accessRun ( by, ReadableChannel.class, channel -> {

            putDataEntry ( zos, "names", makeNames ( channel.getId () ) );
            putDataEntry ( zos, "description", channel.getId ().getDescription () );
            putDirEntry ( zos, "artifacts" );
            putProperties ( zos, "properties.xml", channel.getContext ().getProvidedMetaData () );
            putAspects ( zos, channel.getContext ().getAspectStates ().keySet () );

            // the first run receives all artifacts and filters for the root elements

            putArtifacts ( zos, "artifacts/", channel, channel.getArtifacts (), true );
        } );

        this.channelService.accessRun ( by, TriggeredChannel.class, channel -> {
            putTriggers ( zos, channel );
        } );

        zos.finish (); // don't close stream, since there might be other channels following
    }

    private Set<String> parseNames ( final String data )
    {
        if ( data == null )
        {
            // we must return a modifiable list
            return new HashSet<> ( 1 );
        }

        final Gson gson = new GsonBuilder ().create ();
        final String[] names = gson.fromJson ( data, String[].class );
        return new HashSet<> ( Arrays.asList ( names ) );
    }

    private String makeNames ( final ChannelId channelId )
    {
        final String[] names = channelId.getNames ().toArray ( new String[channelId.getNames ().size ()] );
        if ( names.length == 0 )
        {
            return null;
        }

        Arrays.sort ( names );

        return new GsonBuilder ().create ().toJson ( names );
    }

    @Override
    public void exportChannel ( final String channelId, final OutputStream stream ) throws IOException
    {
        exportChannel ( By.id ( channelId ), stream );
    }

    private void putArtifacts ( final ZipOutputStream zos, final String baseName, final ReadableChannel channel, final Collection<? extends ArtifactInformation> inputArtifacts, final boolean onlyRoot ) throws IOException
    {
        final List<ArtifactInformation> artifacts = new ArrayList<> ( inputArtifacts ); // make new instance in order to sort them
        Collections.sort ( artifacts, Comparator.comparing ( ArtifactInformation::getId ) );

        for ( final ArtifactInformation art : artifacts )
        {
            if ( !art.is ( "stored" ) )
            {
                continue;
            }

            if ( onlyRoot && art.getParentId () != null )
            {
                // only root artifacts in this run
                continue;
            }

            final String name = String.format ( "%s%s/", baseName, art.getId () );

            // make a dir entry

            {
                final ZipEntry ze = new ZipEntry ( name );
                ze.setComment ( art.getName () );

                final FileTime timestamp = FileTime.fromMillis ( art.getCreationTimestamp ().getTime () );

                ze.setLastModifiedTime ( timestamp );
                ze.setCreationTime ( timestamp );
                ze.setLastAccessTime ( timestamp );

                zos.putNextEntry ( ze );
                zos.closeEntry ();
            }

            // put the provided properties

            putProperties ( zos, name + "properties.xml", art.getProvidedMetaData () );
            putDataEntry ( zos, name + "name", art.getName () );

            if ( art.is ( "generator" ) )
            {
                putDataEntry ( zos, name + "generator", art.getVirtualizerAspectId () );
            }

            // put the blob

            try
            {
                channel.getContext ().stream ( art.getId (), in -> {
                    zos.putNextEntry ( new ZipEntry ( name + "data" ) );
                    ByteStreams.copy ( in, zos );
                    zos.closeEntry ();
                } );
            }
            catch ( final Exception e )
            {
                throw new IOException ( "Failed to export artifact", e );
            }

            // further calls will process all artifacts but only get a filtered list

            final List<? extends ArtifactInformation> childs = art.getChildIds ().stream ().map ( id -> channel.getArtifact ( id ) ).filter ( opt -> opt.isPresent () ).map ( opt -> opt.get () ).collect ( Collectors.toList () );

            // put children of this entry

            putArtifacts ( zos, name, channel, childs, false );
        }
    }

    private void putAspects ( final ZipOutputStream zos, final Set<String> aspects ) throws IOException
    {
        final List<String> list = new ArrayList<> ( aspects );
        Collections.sort ( list );

        final StringBuilder sb = new StringBuilder ();

        for ( final String aspect : list )
        {
            sb.append ( aspect ).append ( '\n' );
        }

        putDataEntry ( zos, "aspects", sb.toString () );
    }

    private void importTriggers ( final TriggerExport triggerExport, final TriggeredChannel channel )
    {
        triggerExport.apply ( channel );
    }

    private void putTriggers ( final ZipOutputStream zos, final TriggeredChannel channel ) throws IOException
    {
        final TriggerExport export = TriggerExport.buildFrom ( channel );
        putDataEntry ( zos, "triggers.json", export.toJson () );
    }

    private void putProperties ( final ZipOutputStream zos, final String name, final Map<MetaKey, String> providedMetaData ) throws IOException
    {
        if ( providedMetaData.isEmpty () )
        {
            return;
        }

        zos.putNextEntry ( new ZipEntry ( name ) );

        final XmlHelper xml = new XmlHelper ();

        final Document doc = xml.create ();
        final Element root = doc.createElement ( "properties" );
        doc.appendChild ( root );

        final SortedMap<MetaKey, String> sorted = new TreeMap<> ( providedMetaData );

        for ( final Map.Entry<MetaKey, String> entry : sorted.entrySet () )
        {
            final Element p = XmlHelper.addElement ( root, "property" );
            p.setAttribute ( "namespace", entry.getKey ().getNamespace () );
            p.setAttribute ( "key", entry.getKey ().getKey () );
            p.setTextContent ( entry.getValue () );
        }

        try
        {

            xml.write ( doc, zos );
            zos.closeEntry ();
        }
        catch ( final Exception e )
        {
            throw new IOException ( "Failed to serialize XML", e );
        }
    }

    private void putDirEntry ( final ZipOutputStream zos, String name ) throws IOException
    {
        if ( !name.endsWith ( "/" ) )
        {
            name = name + "/";
        }

        final ZipEntry entry = new ZipEntry ( name );
        zos.putNextEntry ( entry );
        zos.closeEntry ();
    }

    private void putDataEntry ( final ZipOutputStream stream, final String name, final String data ) throws IOException
    {
        if ( data == null )
        {
            return;
        }
        stream.putNextEntry ( new ZipEntry ( name ) );
        stream.write ( data.getBytes ( StandardCharsets.UTF_8 ) );
        stream.closeEntry ();
    }

}


/*******************************************************************************
 * Copyright (c) 2016 IBH SYSTEMS GmbH and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *******************************************************************************/
package org.eclipse.packagedrone.utils.rpm.header;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;

import org.eclipse.packagedrone.utils.rpm.ReadableHeader;
import org.eclipse.packagedrone.utils.rpm.RpmBaseTag;
import org.eclipse.packagedrone.utils.rpm.RpmTag;

public class Header<T extends RpmBaseTag> implements ReadableHeader<T>
{
    @FunctionalInterface
    public interface ArrayAllocator<T>
    {
        public T[] allocate ( int length );
    }

    @FunctionalInterface
    public interface Putter<T extends RpmBaseTag, V>
    {
        public void put ( Header<T> header, T tag, V[] values );
    }

    @FunctionalInterface
    public interface ToShortFunction<T>
    {
        public short applyAsShort ( T value );
    }

    private static final class I18nString
    {
        private final String value;

        public I18nString ( final String value )
        {
            this.value = value;
        }
    }

    private final Map<Integer, Object> entries = new LinkedHashMap<> ();
    private static Charset charset = StandardCharsets.UTF_8;

    public Header ( final HeaderEntry[] entries )
    {
        if ( entries != null )
        {
            for ( final HeaderEntry entry : entries )
            {
                this.entries.put ( entry.getTag (), entry );
            }
        }
    }

    public Header ( final Header<T> other )
    {
        Objects.requireNonNull ( other );

        this.entries.putAll ( other.entries );
    }

    public Header ()
    {
    }

    public int size ()
    {
        return this.entries.size ();
    }

    public void putNull ( final int tag )
    {
        this.entries.put ( tag, null );
    }

    public void putNull ( final T tag )
    {
        this.entries.put ( tag.getValue (), null );
    }

    public void putByte ( final int tag, final byte... value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag, value );
    }

    public void putByte ( final T tag, final byte... value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag.getValue (), value );
    }

    public void putShort ( final int tag, final short... value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag, value );
    }

    public void putShort ( final T tag, final short... value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag.getValue (), value );
    }

    public void putInt ( final int tag, final int... value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag, value );
    }

    public void putInt ( final T tag, final int... value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag.getValue (), value );
    }

    public void putLong ( final int tag, final long... value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag, value );
    }

    public void putLong ( final T tag, final long... value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag.getValue (), value );
    }

    public void putString ( final int tag, final String value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag, value );
    }

    public void putString ( final T tag, final String value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag.getValue (), value );
    }

    public void putStringOptional ( final int tag, final String value )
    {
        if ( value == null )
        {
            return;
        }

        this.entries.put ( tag, value );
    }

    public void putStringOptional ( final T tag, final String value )
    {
        if ( value == null )
        {
            return;
        }

        this.entries.put ( tag.getValue (), value );
    }

    public void putStringArray ( final int tag, final String... value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag, value );
    }

    public void putStringArray ( final T tag, final String... value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag.getValue (), value );
    }

    public void putI18nString ( final int tag, final String... value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag, Arrays.stream ( value ).map ( v -> new I18nString ( v ) ).toArray ( I18nString[]::new ) );
    }

    public void putI18nString ( final T tag, final String... value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag.getValue (), Arrays.stream ( value ).map ( v -> new I18nString ( v ) ).toArray ( I18nString[]::new ) );
    }

    public void putBlob ( final int tag, final byte[] value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag, ByteBuffer.wrap ( value ) );
    }

    public void putBlob ( final int tag, final ByteBuffer value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag, value );
    }

    public void putBlob ( final T tag, final byte[] value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag.getValue (), ByteBuffer.wrap ( value ) );
    }

    public void putBlob ( final T tag, final ByteBuffer value )
    {
        Objects.requireNonNull ( value );

        this.entries.put ( tag.getValue (), value );
    }

    public void putSize ( long value, final T intTag, final T longTag )
    {
        Objects.requireNonNull ( intTag );
        Objects.requireNonNull ( longTag );

        if ( value <= 0 )
        {
            value = 0;
        }

        if ( value > Integer.MAX_VALUE )
        {
            putLong ( longTag, value );
        }
        else
        {
            putInt ( intTag, (int)value );
        }
    }

    public void remove ( final int tag )
    {
        this.entries.remove ( tag );
    }

    public void remove ( final RpmTag tag )
    {
        this.entries.remove ( tag.getValue () );
    }

    public Object get ( final int tag )
    {
        return this.entries.get ( tag );
    }

    public Object get ( final T tag )
    {
        return this.entries.get ( tag.getValue () );
    }

    @Override
    public Optional<Object> getValue ( final T tag )
    {
        return Optional.ofNullable ( get ( tag ) );
    }

    /**
     * Make an array of header entries with given charset
     *

     * Note: Further updates on this instance will not update
     * the returned array. This is actually a copy of the current state.
     *

     *
     * @param charset the charset of choice
     * @return a new array of all header entries, unsorted
     */
    public HeaderEntry[] makeEntries ( Charset charset )
    {
        if ( charset == null )
        {
            throw new IllegalArgumentException ( "'charset' cannot be null" );
        }
        Header.charset = charset;
        return this.entries.entrySet ().stream ().map ( Header::makeEntry ).toArray ( num -> new HeaderEntry[num] );
    }

    /**
     * Make an array of header entries
     *

     * Note: Further updates on this instance will not update
     * the returned array. This is actually a copy of the current state.
     *

     *
     * @return a new array of all header entries, unsorted
     */
    public HeaderEntry[] makeEntries ()
    {
        return makeEntries ( StandardCharsets.UTF_8 );
    }

    private static HeaderEntry makeEntry ( final Map.Entry<Integer, Object> entry )
    {
        final Object val = entry.getValue ();
        final int tag = entry.getKey ();

        if ( val instanceof HeaderEntry )
        {
            return (HeaderEntry)val;
        }

        // NULL

        if ( val == null )
        {
            return new HeaderEntry ( Type.NULL, tag, 0, null );
        }

        // FIXME: CHAR

        // BYTE

        if ( val instanceof byte[] )
        {
            final byte[] value = (byte[])val;
            return new HeaderEntry ( Type.BYTE, tag, value.length, value );
        }

        // SHORT

        if ( val instanceof short[] )
        {
            final short[] value = (short[])val;

            final byte[] data = new byte[value.length * 2];
            final ByteBuffer buffer = ByteBuffer.wrap ( data );
            for ( final short v : value )
            {
                buffer.putShort ( v );
            }

            return new HeaderEntry ( Type.SHORT, tag, value.length, data );
        }

        // INT

        if ( val instanceof int[] )
        {
            final int[] value = (int[])val;

            final byte[] data = new byte[value.length * 4];
            final ByteBuffer buffer = ByteBuffer.wrap ( data );
            for ( final int v : value )
            {
                buffer.putInt ( v );
            }

            return new HeaderEntry ( Type.INT, tag, value.length, data );
        }

        // LONG

        if ( val instanceof long[] )
        {
            final long[] value = (long[])val;

            final byte[] data = new byte[value.length * 8];
            final ByteBuffer buffer = ByteBuffer.wrap ( data );
            for ( final long v : value )
            {
                buffer.putLong ( v );
            }

            return new HeaderEntry ( Type.LONG, tag, value.length, data );
        }

        // STRING

        if ( val instanceof String )
        {
            final String value = (String)val;

            return new HeaderEntry ( Type.STRING, tag, 1, makeStringData ( new ByteArrayOutputStream (), value ).toByteArray () );
        }

        // BLOB

        if ( val instanceof ByteBuffer )
        {
            final ByteBuffer value = (ByteBuffer)val;
            byte[] data;
            if ( value.hasArray () )
            {
                data = value.array ();
            }
            else
            {
                data = new byte[value.remaining ()];
                value.get ( data );
            }
            return new HeaderEntry ( Type.BLOB, tag, data.length, data );
        }

        // STRING_ARRAY

        if ( val instanceof String[] )
        {
            final String[] value = (String[])val;

            return new HeaderEntry ( Type.STRING_ARRAY, tag, value.length, makeStringsData ( new ByteArrayOutputStream (), value ).toByteArray () );
        }

        // I18N_STRING

        if ( val instanceof I18nString[] )
        {
            final I18nString[] value = (I18nString[])val;

            return new HeaderEntry ( Type.I18N_STRING, tag, value.length, makeStringsData ( new ByteArrayOutputStream (), value ).toByteArray () );
        }

        throw new IllegalArgumentException ( String.format ( "Unable to process value type: %s", val.getClass () ) );
    }

    private static <T extends OutputStream> T makeStringData ( final T out, final String string )
    {
        try
        {
            if ( string != null )
            {
                out.write ( string.getBytes (charset) );
            }
            out.write ( 0 );
        }
        catch ( final IOException e )
        {
            throw new RuntimeException ( e );
        }
        return out;
    }

    private static <T extends OutputStream> T makeStringsData ( final T out, final String[] strings )
    {
        for ( final String s : strings )
        {
            makeStringData ( out, s );
        }
        return out;
    }

    private static <T extends OutputStream> T makeStringsData ( final T out, final I18nString[] strings )
    {
        for ( final I18nString s : strings )
        {
            if ( s != null )
            {
                makeStringData ( out, s.value );
            }
            else
            {
                makeStringData ( out, null );
            }
        }
        return out;
    }

    public static <E, V, T extends RpmBaseTag> void putFields ( final Header<T> header, final Collection<E> entries, final T tag, final ArrayAllocator<V> arrayAllocator, final Function<E, V> func, final Putter<T, V> putter )
    {
        if ( entries.isEmpty () )
        {
            return;
        }

        final V[] values = arrayAllocator.allocate ( entries.size () );
        int i = 0;
        for ( final E entry : entries )
        {
            values[i] = func.apply ( entry );
            i++;
        }

        putter.put ( header, tag, values );
    }

    public static <E, T extends RpmBaseTag> void putShortFields ( final Header<T> header, final Collection<E> entries, final T tag, final ToShortFunction<E> func )
    {
        if ( entries.isEmpty () )
        {
            return;
        }

        final short[] values = new short[entries.size ()];
        int i = 0;
        for ( final E entry : entries )
        {
            values[i] = func.applyAsShort ( entry );
            i++;
        }

        header.putShort ( tag, values );
    }

    public static <E, T extends RpmBaseTag> void putIntFields ( final Header<T> header, final Collection<E> entries, final T tag, final ToIntFunction<E> func )
    {
        if ( entries.isEmpty () )
        {
            return;
        }

        final int[] values = new int[entries.size ()];
        int i = 0;
        for ( final E entry : entries )
        {
            values[i] = func.applyAsInt ( entry );
            i++;
        }

        header.putInt ( tag, values );
    }

    public static <E, T extends RpmBaseTag> void putLongFields ( final Header<T> header, final Collection<E> entries, final T tag, final ToLongFunction<E> func )
    {
        if ( entries.isEmpty () )
        {
            return;
        }

        final long[] values = new long[entries.size ()];
        int i = 0;
        for ( final E entry : entries )
        {
            values[i] = func.applyAsLong ( entry );
            i++;
        }

        header.putLong ( tag, values );
    }

}