/+ dub.sdl:
    name "A"
    dependency "dcomp" version=">=0.1.2"
+/

import std.stdio, std.datetime, std.range, std.algorithm, std.conv;

// import dcomp.scanner, dcomp.numeric, dcomp.algorithm, dcomp.array;

int[4] normalize(int[4] x) {
    int[4] res = x[].rotator.minPos!((a, b) => cmp(a, b) == -1).front.array;
    return res;
}

//duplicate number
int num(int[4] x) {
    return x[].rotator.count!(a => equal(a, x[])).to!int;
}

long F(int n, int k) {
    if (n < k || n < 0) return 0;
    //n, n-1, ..., n-k+1
    return iota!long(n, n-k, -1).reduce!"a*b";
}

int[4][] col;
int[int[4]] colMp;

long cnt(int a, int b, int[4][4] x) {
    //[0,,a)
    colMp[col[b]]--;
    int[int[4]] mp;
    x.each!(a => mp[a]++);
    long ans = 1;
    foreach (v, k; mp) {
        if (!(v in colMp)) {
            ans = 0;
            continue;
        }
        int n = colMp[v];
        int m = num(v);
        //nからk個選ぶ,それぞれ重複度m
        ans *= pow(m, k);
        ans *= F(n, k);
    }
    
    colMp[col[b]]++;
    return ans;
}

int main(string[] argv) {
    int[4] test;
    Scanner sc = new Scanner();
    int n;
    sc.read(n);
    col = new int[4][](n);
    foreach (ref v; col) {
        sc.read(v);
        v = v.normalize;
    }

    long ans = 0;
    foreach (i; 0..n) {
        foreach (j; 0..i) {
            int[4] a = col[i], right = col[j];
            foreach (b; right[].rotator) {
                int[4][4] x;
                x[0] = [a[1], a[0], b[1], b[0]];
                x[1] = [a[2], a[1], b[0], b[3]];
                x[2] = [a[3], a[2], b[3], b[2]];
                x[3] = [a[0], a[3], b[2], b[1]];
                x[].each!((ref a) => a = a.normalize);
                ans += cnt(i, j, x);
            }
        }
        colMp[col[i]]++;
    }
    writeln(ans);
    return 0;
}
/* IMPORT /Users/yosupo/Program/dcomp/source/dcomp/array.d */
// module dcomp.array;

// import dcomp.memory;

struct FastAppender(A, bool useMyPool = false) {
    @disable this(this); //this is not reference type(don't copy!)

    import std.algorithm : max;
    import std.range.primitives : ElementEncodingType;
    import core.stdc.string : memcpy;

    private alias T = ElementEncodingType!A;
    private T* _data;
    private size_t len, cap;

    this(T[]* arr) {
        _data = (*arr).ptr;
        len = (*arr).length;
        cap = (*arr).length;
    }
    void reserve(size_t nlen) {
        import core.memory : GC;
        if (nlen <= cap) return;

        void* nx;
        if (useMyPool) {
            nx = nowPool.malloc(nlen * T.sizeof);
        } else {
            nx = GC.malloc(nlen * T.sizeof, GC.BlkAttr.NO_SCAN | GC.BlkAttr.APPENDABLE);
        }
        cap = nlen;
        if (len) memcpy(nx, _data, len * T.sizeof);
        _data = cast(T*)(nx);
    }
    void opOpAssign(string op : "~")(T item) {
        if (len == cap) {
            reserve(max(4, cap*2));
        }
        _data[len++] = item;
    }
    void clear() {
        len = 0;
    }
    T[] data() {
        return (_data) ? _data[0..len] : null;
    }
}

T[N] toStaticArray(T, int N)(T[N] a) {return a;}
bool cmpStaticArray(T, int N)(in T[N] a, in T[N] b) {
    foreach (i; 0..N) {
        if (a[i] != b[i]) return a[i] < b[i];
    }
    return false;
}

/* IMPORT /Users/yosupo/Program/dcomp/source/dcomp/scanner.d */
// module dcomp.scanner;

class Scanner {
    import std.stdio : File, stdin;
    import std.conv : to;
    import std.range : front, popFront, array, ElementType;
    import std.array : split;
    import std.traits : isSomeChar, isStaticArray, isArray; 
    import std.algorithm : map;
    File f;
    this(File f = stdin) {
        this.f = f;
    }
    string[] buf;
    private bool succ() {
        while (!buf.length) {
            if (f.eof) return false;
            buf = f.readln.split;
        }
        return true;
    }
    private bool readSingle(T)(ref T x) {
        if (!succ()) return false;
        static if (isArray!T) {
            alias E = ElementType!T;
            static if (isSomeChar!E) {
                //string or char[10] etc
                x = buf.front;
                buf.popFront;
            } else {
                static if (isStaticArray!T) {
                    //static
                    assert(buf.length == T.length);
                }
                x = buf.map!(to!E).array;
                buf.length = 0;                
            }
        } else {
            x = buf.front.to!T;
            buf.popFront;            
        }
        return true;
    }
    int read(T, Args...)(ref T x, auto ref Args args) {
        if (!readSingle(x)) return 0;
        static if (args.length == 0) {
            return 1;
        } else {
            return 1 + read(args);
        }
    }
}

unittest {
    import std.path : buildPath;
    import std.file : tempDir;
    import std.algorithm : equal;
    import std.stdio : File;
    string fileName = buildPath(tempDir, "kyuridenanmaida.txt");
    auto fout = File(fileName, "w");
    fout.writeln("1 2 3");
    fout.writeln("ab cde");
    fout.writeln("1.0 1.0 2.0");
    fout.close;
    Scanner sc = new Scanner(File(fileName, "r"));
    int a;
    int[2] b;
    char[2] c;
    string d;
    double e;
    double[] f;
    sc.read(a, b, c, d, e, f);
    assert(a == 1);
    assert(equal(b[], [2, 3]));
    assert(equal(c[], "ab"));
    assert(equal(d, "cde"));
    assert(e == 1.0);
    assert(equal(f, [1.0, 2.0]));
}
/* IMPORT /Users/yosupo/Program/dcomp/source/dcomp/memory.d */
// module dcomp.memory;

MemoryPool _nowPool;

@property void nowPool(MemoryPool newPool) {
    _nowPool = newPool;
}
@property MemoryPool nowPool() {
    return _nowPool;
}

class MemoryPool {
    import core.memory : GC;
    import std.algorithm : max;

    static immutable Allign = 16;
    GC.BlkInfo[] blks;
    size_t e, idx, pos;
    this(size_t e) {
        this.e = e;
        idx = pos = 0;
    }
    void assign(size_t sz) {
        blks ~= GC.qalloc(sz);
    }
    void* malloc(size_t sz) {   
        sz = (sz + Allign-1) / Allign * Allign;
        while (idx < blks.length && blks[idx].size < pos + sz) {
            idx++; pos = 0;
        }

        if (idx == blks.length) {
            assign(max(e, sz)); pos = 0;
        }
        pos += sz;
        assert(pos <= blks[idx].size);
        return cast(void *)(cast(byte *)(blks[idx].base) + pos - sz);
    }
    void allFree() {
        idx = pos = 0;
    }
}
/* IMPORT /Users/yosupo/Program/dcomp/source/dcomp/algorithm.d */
// module dcomp.algorithm;

//[0,0,0,...,1,1,1]で、初めて1となる場所を探す。pred(l) == 0, pred(r) == 1と仮定
T binSearch(alias pred, T)(T l, T r) {
    while (r-l > 1) {
        T md = (l+r)/2;
        if (!pred(md)) l = md;
        else r = md;
    }
    return r;
}

import std.range.primitives;

Rotator!Range rotator(Range)(Range r)
if (isForwardRange!Range && hasLength!Range) {
    return typeof(return)(r);
}

struct Rotator(Range)
if (isForwardRange!Range && hasLength!Range) {
    size_t cnt;
    Range start, now;
    this(Range r) {
        cnt = 0;
        start = r.save;
        now = r.save;
    }
    this(this) {
        start = start.save;
        now = now.save;
    }
    @property bool empty() {
        return now.empty;
    }
    @property auto front() {
        assert(!now.empty);
        import std.range : take, chain;
        return chain(now, start.take(cnt));
    }
    @property Rotator!Range save() {
        return this;
    }
    void popFront() {
        cnt++;
        now.popFront;
    }
}
/* IMPORT /Users/yosupo/Program/dcomp/source/dcomp/numeric.d */
// module dcomp.numeric;


T pow(T, U)(T x, U n, T e) {
    while (n) {
        if (n & 1) e *= x;
        x *= x;
        n /= 2;
    }
    return e;
}
T pow(T, U)(T x, U n) {
    return pow(x, n, T(1));
}

T lcm(T)(in T a, in T b) {
    import std.numeric : gcd, abs;
    return a / gcd(a,b) * b;
}

//a*T[0]+b*T[1]=T[2], T[2]=gcd
//todo: to binary extgcd
T[3] extGcd(T)(T a, T b) {
    if (b==0) {
        return [1, 0, a];
    } else {
        auto e = extGcd(b, a%b);
        return [e[1], e[0]-a/b*e[1], e[2]];
    }
}

struct ModInt(uint MD) {
    import std.conv : to;
    uint v;
    this(int v) {this.v = (v%MD+MD)%MD;}
    this(long v) {this.v = (v%MD+MD)%MD;}
    auto normS(uint x) {return (x<MD)?x:x-MD;}
    auto make(uint x) {ModInt m; m.v = x; return m;}
    auto opBinary(string op:"+")(ModInt r) {return make(normS(v+r.v));}
    auto opBinary(string op:"-")(ModInt r) {return make(normS(v+MD-r.v));}
    auto opBinary(string op:"*")(ModInt r) {return make((v.to!long*r.v%MD).to!uint);}
    auto opBinary(string op:"/")(ModInt r) {return this*inv(r);}
    auto opOpAssign(string op)(ModInt r) {return mixin ("this=this"~op~"r");}
    static ModInt inv(ModInt x) {return ModInt(extGcd(x.v, MD)[0]);}
    string toString() {return v.to!string;}
}

// f([10]) = [1*] 
// &演算での畳み込みを内積に変換する
T[] zeta(T)(T[] v, bool rev) {
    import core.bitop : bsr;
    int n = bsr(v.length);
    assert(1<<n == v.length);
    foreach (fe; 0..n) {
        foreach (i, _; v) {
            if (i & (1<<fe)) continue;
            if (!rev) {
                v[i] += v[i|(1<<fe)];
            } else {
                v[i] -= v[i|(1<<fe)];
            }
        }
    }
    return v;
}

// xor演算での畳み込みを内積に変換する、サイズ2のFFT
T[] hadamard(T)(T[] v, bool rev) {
    import core.bitop : bsr;
    int n = bsr(v.length);
    assert(1<<n == v.length);
    foreach (fe; 0..n) {
        foreach (i, _; v) {
            if (i & (1<<fe)) continue;
            auto l = v[i], r = v[i|(1<<fe)];
            if (!rev) {
                v[i] = l+r;
                v[i|(1<<fe)] = l-r;
            } else {
                v[i] = (l+r)/2;
                v[i|(1<<fe)] = (l-r)/2;
            }
        }
    }
    return v;
}


Measured memory_bytes: 1404