/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.neo4j.io.pagecache.randomharness;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import org.neo4j.io.pagecache.PageCursor;
import org.neo4j.io.pagecache.impl.muninn.MuninnPageCache;

public class StandardRecordFormat extends RecordFormat {
    @Override
    public int getRecordSize() {
        return 32;
    }

    @Override
    public Record createRecord(Path path, int recordId, int page, int offset) {
        return new StandardRecord(path, recordId, page, offset);
    }

    @Override
    public Record readRecord(PageCursor cursor) throws IOException {
        int offset = cursor.getOffset();
        byte t;
        byte f;
        short f1;
        int recordId;
        int page;
        int recordOffset;
        long f2;
        do {
            cursor.setOffset(offset);
            t = cursor.getByte();
            f = cursor.getByte();
            f1 = cursor.getShort();
            recordId = cursor.getInt();
            f2 = cursor.getLong();
            page = cursor.getInt();
            recordOffset = cursor.getInt();
            cursor.getLong(); // empty space
        } while (cursor.shouldRetry());
        return new StandardRecord(t, f, f1, recordId, f2, page, recordOffset);
    }

    @Override
    public Record zeroRecord() {
        byte z = MuninnPageCache.ZERO_BYTE;
        short sz = (short) ((z << 8) + z);
        int iz = (sz << 16) + sz;
        long lz = (((long) iz) << 32) + iz;
        return new StandardRecord(z, z, sz, iz, lz, 0, 0);
    }

    @Override
    public void write(Record record, PageCursor cursor) {
        StandardRecord r = (StandardRecord) record;
        byte[] pathBytes = r.path.toString().getBytes(StandardCharsets.UTF_8);
        byte fileByte = pathBytes[pathBytes.length - 1];
        cursor.putByte(r.type);
        cursor.putByte(fileByte);
        cursor.putShort(r.fill1);
        cursor.putInt(r.recordId);
        cursor.putLong(r.fill2);
        cursor.putInt(r.page);
        cursor.putInt(r.offset);
        cursor.putLong(0);
    }

    static final class StandardRecord implements Record {
        final byte type;
        final Path path;
        final int recordId;
        final short fill1;
        final long fill2;
        private final int page;
        private final int offset;

        StandardRecord(Path path, int recordId, int page, int offset) {
            this.type = 42;
            this.path = path;
            this.recordId = recordId;
            this.page = page;
            this.offset = offset;
            int fileHash = path.hashCode();

            int a = xorshift(fileHash ^ xorshift(recordId));
            int b = xorshift(a);
            int c = xorshift(b);
            long d = b;
            d = d << 32;
            d += c;
            fill1 = (short) a;
            fill2 = d;
        }

        StandardRecord(byte type, byte fileName, short fill1, int recordId, long fill2, int page, int offset) {
            this.type = type;
            this.path = fileName == 0 ? null : Path.of(new String(new byte[] {fileName}));
            this.fill1 = fill1;
            this.recordId = recordId;
            this.page = page;
            this.offset = offset;
            this.fill2 = fill2;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            StandardRecord record = (StandardRecord) o;

            return type == record.type
                    && recordId == record.recordId
                    && fill1 == record.fill1
                    && fill2 == record.fill2
                    && filesEqual(record);
        }

        private boolean filesEqual(StandardRecord record) {
            if (path == record.path) {
                return true;
            }
            if (path == null || record.path == null) {
                return false;
            }
            // We only look at the last letter of the path, because that's all that we can store in the record.
            byte[] thisPath = path.toString().getBytes(StandardCharsets.UTF_8);
            byte[] thatPath = record.path.toString().getBytes(StandardCharsets.UTF_8);
            return thisPath[thisPath.length - 1] == thatPath[thatPath.length - 1];
        }

        @Override
        public int hashCode() {
            int result = type;
            result = 31 * result + (path != null ? path.hashCode() : 0);
            result = 31 * result + recordId;
            result = 31 * result + (int) fill1;
            result = 31 * result + (int) (fill2 ^ (fill2 >>> 32));
            return result;
        }

        private static int xorshift(int x) {
            x ^= x << 6;
            x ^= x >>> 21;
            return x ^ (x << 7);
        }

        @Override
        public String toString() {
            return format(type, path, recordId, fill1, fill2);
        }

        public String format(byte type, Path path, int recordId, short fill1, long fill2) {
            return String.format(
                    "Record%s on page: %s offset: %s [file=%s, recordId=%s; %04x %016x]",
                    type, page, offset, path, recordId, fill1, fill2);
        }
    }
}
